Libraries¶

In [142]:
#Instalar librerias
import importlib.util
import sys

def check_and_install_library(library_name_list: list):
    for library_name in library_name_list:
        spec = importlib.util.find_spec(library_name)
        if spec is None:
            print(f"Library '{library_name}' not found. Installing...")
            try:
                # Use pip to install the library
                # The ! prefix runs shell commands from within Jupyter
                !{sys.executable} -m pip install {library_name}
                print(f"Library '{library_name}' installed successfully.")
            except Exception as e:
                print(f"Error installing '{library_name}': {e}")
        # else:
            # print(f"Library '{library_name}' is already installed.")
    return

library_name_list = ['pandas', 'numpy', 'jupyter', 'notebook', 'yfinance', 'matplotlib.pyplot', 'json', 'ipynb', 'import_ipynb', 'datetime', 
                     'ipywidgets', 'IPython.display', 'anywidget',# widgets
                     'nbconvert', 'pandoc', 'TeX'] #To export to HTML and PDF

check_and_install_library(library_name_list)



import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

import ipywidgets as widgets
from IPython.display import display, clear_output

#from datetime import date
from datetime import datetime

import json
#import ipynb


from dataclasses import dataclass

import nbconvert

import import_ipynb
import functions # => .../functions.ipynb     file attached
importlib.reload(functions) # Reloads the module

from plotly.graph_objects import FigureWidget

%reload_ext autoreload
%autoreload 2
Library 'TeX' not found. Installing...
Requirement already satisfied: TeX in c:\users\egarcia\appdata\local\programs\python\python312\lib\site-packages (1.8)
Library 'TeX' installed successfully.

Dates¶

In [2]:
#Dates
global start_date
start_date = datetime(2021, 4, 25) # <- date in which brokerage account started
today = datetime.today()
today = today.replace(hour=0, minute=0, second=0, microsecond=0)
no_days = today - start_date
print(f" \
      Start Date: {start_date},\n \
      End Date: {today},\n \
      number of days {no_days}")
       Start Date: 2021-04-25 00:00:00,
       End Date: 2026-01-06 00:00:00,
       number of days 1717 days, 0:00:00

Tickers¶

Background: From an initial list of 45 assets (tickers) choose a number greather than 5 shuch that they are diversified among industries, liquidity, heterogeneousity, etc.

In [3]:
risk_free = 0.04152 #10-year treasury

# Open list of tickers from original Robinhood's file_df_df
file_name = "Robinhood.xlsx" # Enrique
# file_df = "App_GBM_Detalle_Portafolio_USA_1763936603841.xlsx"  # Beto
file_df = pd.read_excel(file_name)

tickers = file_df[["Ticker"]]
tickers = list(tickers["Ticker"])

no_assets = len(tickers)

print(f"Tickers: {tickers}")
print(f"Number of Assets: {no_assets}")
Tickers: ['QYLD', 'NVDA', 'PBR', 'PLTR', 'MSFT', 'AAPL', 'NU', 'DIS', 'VOOV', 'TSM', 'GLD', 'QQQM', 'VOOG', 'KO', 'SOFI', 'QSR', 'UNH', 'TSLA', 'SPYD', 'SERV', 'SOXX', 'VOO', 'OMAB', 'VYM', 'VGT', 'GOOGL', 'CME', 'GOOG', 'META', 'BAC', 'CRWD', 'NFLX', 'MCD', 'SPYG', 'VUG', 'ADBE', 'MAR', 'VTV', 'CAT', 'LMT', 'ASML', 'BRK-B', 'SPG', 'PSA', 'HD']
Number of Assets: 45

Fundamental Analysis ALL Tickers¶

TS302_Stock Full Analysis.ipynb

Criteria to choose stocks:

|Ratio | Formula | Criteria (great if) |Attribute :--- | :---: | :---: |:---: P/E Ratio |Current Stock Price / Earnings per Share | between 10 and 20 (fair valuation) |info.trailingPE P/B Ratio |Price per Share / Book Value | < 3 (not overvalued) |info.priceToBook ROIC (%) |NOPAT / Total Inv. Capital (=Debt+Equity-Assets) | > 15% |functions.get_roic('AAPL') D/E (%) |Debt / Equity | < 100% (0%-200%) |info.debtToEquity EPS (USD) |Net Income / Shares Outstanding | > 10% CAGR |info.epsForward ROE Ratio | Net Income / Equity | > 0.15 |info.returnOnEquity EBIT Margin (%) |EBIT / Sales | > 10% |functions.get_ebit_margin("AAPL") Gross Margin Ratio |Sales - COGS / Sales | > 0.40 (0.35-0.65) |info.grossMargins Net Margin (%) |Net Income / Revenue | (15%-25%) |functions.get_net_margin("AAPL") Current Ratio |Current Assets / Current Liabilities | (1.5-2.0) |info.currentRatio Earning Growth Ratio (PEG Ratio) | P/E Ratio / Annual EPS Growth Rate (% as a whole number) | < 1.0 (undervalued) | info.earningsGrowth

ROIC: Return on Invested Capital

COGS: Cost of Goods Sold

CAGR: Compound Annual Growth Rate

NOPAT: Net Operating Profit After Tax (NOPAT) = Operating Income(or Operating Profit) * (1 - Tax Rate)

Book Value: = (Total Assets - Total Liabilities - Preferred Stock) / Number of Outstanding Common Shares

Overall Risk: Overall assessment including Audit Risk, Board Risk, Compensation Risk, Share Holder Rights Risk. 10 is max, 0 is min. Can be seen individually in .info

yfinance provides:

  • hist()
  • info
  • Income Statement
  • Financial Statement
In [4]:
## Fundamental Analysis for all Tickers based on Financial Ratios

# Call the get_fundamental_analysis() function (...be patient takes ~71sec)
fa_df = functions.get_fundamental_analysis(tickers, start_date, today, showLogs = "no")
display(fa_df)  #Result filtered by: ['Sector','P/E Ratio','P/B Ratio']
# sys.stdout = original_stdout
Name Sector Industry CAGR_% P/E Ratio P/B Ratio ROIC_% D/E_% EPS_usd ROE Ratio ... Gross Margin_ratio Net Margin_% Current Ratio Overall Risk Beta EBITDA_usd EBITDA Margins Ratio Earning Growth Ratio Revenue Growth Ratio Operating Margins Ratio
Ticker
NFLX Netflix, Inc. Communication Services Entertainment 13.21 38.108330 14.932244 28.58 65.822 3.242550 0.42861 ... 0.48085 22.34 1.332 9.0 1.711 1.296965e+10 0.29899 0.087 0.172 0.28220
GOOG Alphabet Inc. Communication Services Internet Content & Information 23.97 31.355732 9.906034 30.69 11.424 11.197380 0.35450 ... 0.59172 28.60 1.747 0.0 1.086 1.451740e+11 0.37661 0.353 0.159 0.30512
GOOGL Alphabet Inc. Communication Services Internet Content & Information 24.10 31.247780 9.881684 30.69 11.424 11.197380 0.35450 ... 0.59172 28.60 1.747 10.0 1.086 1.451740e+11 0.37661 0.353 0.159 0.30512
META Meta Platforms, Inc. Communication Services Internet Content & Information 18.12 29.149998 8.557937 33.52 26.311 30.418530 0.32643 ... 0.82013 37.91 1.978 10.0 1.287 9.839900e+10 0.51937 -0.826 0.262 0.40075
DIS The Walt Disney Company Communication Services Entertainment -9.24 16.628279 1.859483 7.32 39.632 7.355800 0.12203 ... 0.37764 13.14 0.710 3.0 1.442 1.941900e+10 0.20566 1.873 -0.005 0.11868
TSLA Tesla, Inc. Consumer Cyclical Auto Manufacturers 13.78 311.496550 18.774212 10.61 17.082 2.203830 0.06791 ... 0.17007 7.30 2.066 10.0 1.835 1.076800e+10 0.11260 -0.371 0.116 0.06628
MAR Marriott International, Inc. Consumer Cyclical Lodging 17.93 32.774500 -26.863880 24.47 0.000 11.383310 0.00000 ... 0.81554 9.46 0.467 7.0 1.157 4.600000e+09 0.66919 0.290 0.056 0.65934
MCD McDonald's Corporation Consumer Cyclical Restaurants 8.03 25.585323 -98.735590 20.02 0.000 13.229860 0.00000 ... 0.57425 31.72 1.000 5.0 0.531 1.429200e+10 0.54417 0.016 0.030 0.46906
QSR Restaurant Brands International Inc. Consumer Cyclical Restaurants 3.70 23.666666 6.470816 10.81 306.718 4.015010 0.25245 ... 0.33528 12.15 1.059 4.0 0.605 2.701000e+09 0.29156 0.217 0.069 0.27726
HD The Home Depot, Inc. Consumer Cyclical Home Improvement Retail 4.08 23.471350 28.257370 24.64 544.586 15.095210 1.62909 ... 0.33355 9.28 1.051 1.0 1.072 2.558700e+10 0.15396 -0.014 0.028 0.12945
KO The Coca-Cola Company Consumer Defensive Beverages - Non-Alcoholic 8.37 22.496689 9.354261 20.50 144.771 3.220510 0.42442 ... 0.61633 22.59 1.211 3.0 0.387 1.630700e+10 0.34213 0.301 0.051 0.32373
SOXX iShares Semiconductor ETF ETF, others ETF, others 18.82 41.787422 0.749979 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VGT Vanguard Information Technology Index Fund ETF... ETF, others ETF, others 16.07 39.488186 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VUG Vanguard Growth Index Fund ETF Shares ETF, others ETF, others 13.41 39.276190 2.263974 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VOOG Vanguard S&P 500 Growth Index Fund ETF Shares ETF, others ETF, others 13.71 35.788900 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
QYLD Global X NASDAQ 100 Covered Call ETF ETF, others ETF, others 7.28 34.672573 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
QQQM Invesco NASDAQ 100 ETF ETF, others ETF, others 14.21 34.027912 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
SPYG State Street SPDR Portfolio S&P 500 Growth ETF ETF, others ETF, others 13.77 33.924810 1.701520 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VOO Vanguard S&P 500 ETF ETF, others ETF, others 12.82 29.083656 1.618149 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VOOV Vanguard S&P 500 Value Index Fund ETF Shares ETF, others ETF, others 10.85 23.608147 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VTV Vanguard Value Index Fund ETF Shares ETF, others ETF, others 10.73 21.379675 2.568654 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
VYM Vanguard High Dividend Yield Index Fund ETF Sh... ETF, others ETF, others 10.95 20.940632 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
SPYD State Street SPDR Portfolio S&P 500 High Divid... ETF, others ETF, others 6.48 15.968643 0.000000 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
GLD SPDR Gold Shares ETF, others ETF, others 20.98 0.000000 2.404230 0.00 0.000 0.000000 0.00000 ... 0.00000 0.00 0.000 0.0 0.000 0.000000e+00 0.00000 0.000 0.000 0.00000
PBR Petróleo Brasileiro S.A. - Petrobras Energy Oil & Gas Integrated 36.76 5.435185 1.940786 8.81 88.498 2.536670 0.19022 ... 0.48152 8.23 0.819 0.0 -0.032 1.910370e+11 0.38872 0.005 -0.013 0.36224
SOFI SoFi Technologies, Inc. Financial Services Credit Services 12.71 52.285717 4.017012 0.00 31.999 0.572950 0.08593 ... 0.82510 19.09 1.150 9.0 1.932 0.000000e+00 0.00000 1.059 0.378 0.15598
NU Nu Holdings Ltd. Financial Services Banks - Regional 12.45 34.500004 8.236915 0.00 0.000 0.911740 0.27800 ... 0.00000 23.85 0.000 0.0 1.083 0.000000e+00 0.00000 0.409 0.363 0.58215
CME CME Group Inc. Financial Services Financial Data & Stock Exchanges 10.76 26.678951 3.511150 13.50 13.372 11.683760 0.13346 ... 1.00000 57.52 1.021 10.0 0.291 4.494100e+09 0.70383 -0.004 -0.030 0.63386
BRK-B Berkshire Hathaway Inc. Financial Services Insurance - Diversified 13.85 15.947536 0.001027 12.90 18.166 24.190810 0.10170 ... 0.24361 20.98 2.722 10.0 0.710 1.038640e+11 0.27911 0.172 0.021 0.41103
BAC Bank of America Corporation Financial Services Banks - Diversified 10.81 15.543715 1.499038 0.00 0.000 4.357060 0.09871 ... 0.00000 26.63 0.000 4.0 1.295 0.000000e+00 0.00000 0.315 0.126 0.35293
UNH UnitedHealth Group Incorporated Healthcare Healthcare Plans -1.44 17.822823 3.234996 12.62 75.734 17.767490 0.17476 ... 0.19701 3.60 0.823 8.0 0.425 2.924200e+10 0.06720 -0.602 0.122 0.03813
CAT Caterpillar Inc. Industrials Farm & Heavy Construction Machinery 25.49 31.643555 13.956912 21.86 201.046 22.386720 0.46283 ... 0.30120 16.65 1.384 4.0 1.568 1.395800e+10 0.21583 -0.036 0.095 0.17746
LMT Lockheed Martin Corporation Industrials Aerospace & Defense 10.00 28.483854 19.035872 25.81 358.987 29.223770 0.62776 ... 0.08252 7.51 1.129 5.0 0.245 7.257000e+09 0.09894 0.022 0.088 0.11693
OMAB Grupo Aeroportuario del Centro Norte, S.A.B. d... Industrials Airports & Air Services 24.74 17.794788 75.197770 29.05 132.873 7.859000 0.54332 ... 0.74202 32.70 1.138 0.0 0.611 9.810583e+09 0.61446 0.092 0.061 0.61165
SERV Serve Robotics Inc. Industrials Specialty Industrial Machinery -12.53 0.000000 3.024809 -355.90 1.461 -1.773330 -0.47177 ... 0.00000 -2162.29 17.214 10.0 0.000 -8.331123e+07 0.00000 0.000 2.095 -50.67831
PSA Public Storage Real Estate REIT - Industrial 3.31 27.092419 9.225600 12.74 106.758 10.247400 0.19933 ... 0.72701 44.13 0.269 5.0 0.991 3.379439e+09 0.70476 0.213 0.031 0.46947
SPG Simon Property Group, Inc. Real Estate REIT - Retail 15.45 26.653566 25.888590 13.78 884.863 6.882500 0.82455 ... 0.81993 39.75 0.592 10.0 1.400 4.549947e+09 0.73919 0.274 0.082 0.50757
PLTR Palantir Technologies Inc. Technology Software - Infrastructure 52.23 395.545440 62.943940 9.44 3.520 1.010140 0.19504 ... 0.80808 16.13 6.427 10.0 1.545 8.757970e+08 0.22478 2.000 0.628 0.33296
NVDA NVIDIA Corporation Technology Semiconductors 70.14 46.564354 38.454617 90.19 9.102 7.566360 1.07359 ... 0.70050 55.85 4.468 8.0 2.314 1.126960e+11 0.60220 0.667 0.625 0.63169
ASML ASML Holding N.V. Technology Semiconductor Equipment & Materials 14.83 43.291855 21.398302 79.56 14.240 30.810055 0.53852 ... 0.52711 26.79 1.308 0.0 1.341 1.215790e+10 0.37743 0.038 0.007 0.32842
AAPL Apple Inc. Technology Consumer Electronics 16.30 35.825737 53.548386 82.29 152.411 9.155080 1.71422 ... 0.46905 26.92 0.893 1.0 1.093 1.447480e+11 0.34782 0.912 0.079 0.31647
MSFT Microsoft Corporation Technology Software - Infrastructure 14.36 33.678776 9.681614 27.78 33.154 18.742380 0.32241 ... 0.68764 36.15 1.401 5.0 1.073 1.664370e+11 0.56647 0.127 0.184 0.48873
TSM Taiwan Semiconductor Manufacturing Company Lim... Technology Semiconductors 25.15 33.324715 52.480240 36.86 20.436 13.083090 0.34657 ... 0.58976 40.02 2.693 0.0 1.274 2.484714e+12 0.68423 0.391 0.303 0.50578
ADBE Adobe Inc. Technology Software - Application -8.96 19.842012 11.895383 45.27 57.197 26.340080 0.55426 ... 0.89268 25.85 0.996 1.0 1.526 9.551333e+09 0.40184 0.172 0.105 0.36503
CRWD CrowdStrike Holdings, Inc. Technology Software - Infrastructure 16.29 0.000000 28.652567 -25.06 20.154 4.834480 -0.08815 ... 0.74277 -0.49 1.811 10.0 1.029 -9.382400e+07 -0.02055 0.000 0.222 -0.05589

45 rows × 21 columns

In [5]:
ratio_criteria= {
    'CAGR_%': (">", 10.0), # %
    'P/E Ratio': ("between", (10.0, 20.0)), # ratio
    'P/B Ratio': ("<", 3.0), # ratio
    'ROIC_%': (">", 15.0), # %
    'D/E_%': ("<", 100.0), # %
    'EPS_usd': (">", 10.0), # USD. # 10% of asset's CAGR. Will automatically trigger 10% CAGR logic in the function
    'ROE Ratio': (">", 0.15), # ratio
    'EBIT Margin_%': (">", 10.0), # %
    'Gross Margin_ratio': (">", 0.40), # ratio
    'Net Margin_%': (">", 15.0), # %
    'Current Ratio': (">", 1.5), # ratio
    'Overall Risk': ("<", 5.0), # 10 is max, 0 is min.
    'Beta': ("between", (0.0, 1.0)),
    'EBITDA_usd': (">", 0.00), # USD
    'EBITDA Margins Ratio': (">", 0.15), # ratio
    'Earning Growth Ratio': ("<", 1.0), # ratio -  PEG < 1.0 (Undervalued): This is the ideal range, as it suggests the stock price is low relative to its earnings growth potential, indicating a potentially attractive investment opportunity
    'Revenue Growth Ratio': (">", 0.15), # ratio
    'Operating Margins Ratio': (">", 0.15) # ratio
}
In [6]:
# Plot financial Ratios
functions.plot_ratios(fa_df, ratio_criteria, plots_per_row = 3)
No description has been provided for this image
In [7]:
# Generate the data
scorecard = functions.generate_scorecard(fa_df, ratio_criteria)

# Display with a gradient highlight on the Score column
print("\n--- STOCK SELECTION SCORECARD ---")
display(scorecard.style.background_gradient(subset=['Score %'], cmap='RdYlGn'))
--- STOCK SELECTION SCORECARD ---
  CAGR_% P/E Ratio P/B Ratio ROIC_% D/E_% EPS_usd ROE Ratio EBIT Margin_% Gross Margin_ratio Net Margin_% Current Ratio Overall Risk Beta EBITDA_usd EBITDA Margins Ratio Earning Growth Ratio Revenue Growth Ratio Operating Margins Ratio Score % Name Sector Industry
Ticker                                            
GOOG ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ✅ 83.330000 Alphabet Inc. Communication Services Internet Content & Information
TSM ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ✅ 83.330000 Taiwan Semiconductor Manufacturing Company Limited Technology Semiconductors
META ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ 77.780000 Meta Platforms, Inc. Communication Services Internet Content & Information
OMAB ✅ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ 77.780000 Grupo Aeroportuario del Centro Norte, S.A.B. de C.V. Industrials Airports & Air Services
GOOGL ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ 77.780000 Alphabet Inc. Communication Services Internet Content & Information
NVDA ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ 77.780000 NVIDIA Corporation Technology Semiconductors
BRK-B ✅ ✅ ✅ ❌ ✅ ✅ ❌ ✅ ❌ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ❌ ✅ 72.220000 Berkshire Hathaway Inc. Financial Services Insurance - Diversified
ASML ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ 72.220000 ASML Holding N.V. Technology Semiconductor Equipment & Materials
MSFT ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ❌ ✅ ✅ ✅ ✅ ✅ 72.220000 Microsoft Corporation Technology Software - Infrastructure
NFLX ✅ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ❌ ✅ ✅ ✅ ✅ ✅ 72.220000 Netflix, Inc. Communication Services Entertainment
ADBE ❌ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ 72.220000 Adobe Inc. Technology Software - Application
KO ❌ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ 66.670000 The Coca-Cola Company Consumer Defensive Beverages - Non-Alcoholic
MCD ❌ ❌ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ❌ ✅ 66.670000 McDonald's Corporation Consumer Cyclical Restaurants
AAPL ✅ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ 66.670000 Apple Inc. Technology Consumer Electronics
CAT ✅ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ 61.110000 Caterpillar Inc. Industrials Farm & Heavy Construction Machinery
CME ✅ ❌ ❌ ❌ ✅ ✅ ❌ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ❌ ✅ 61.110000 CME Group Inc. Financial Services Financial Data & Stock Exchanges
PBR ✅ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ 61.110000 Petróleo Brasileiro S.A. - Petrobras Energy Oil & Gas Integrated
MAR ✅ ❌ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ❌ ❌ ❌ ❌ ✅ ✅ ✅ ❌ ✅ 61.110000 Marriott International, Inc. Consumer Cyclical Lodging
PLTR ✅ ❌ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ❌ ✅ ✅ 61.110000 Palantir Technologies Inc. Technology Software - Infrastructure
PSA ❌ ❌ ❌ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅ ✅ ✅ ❌ ✅ 55.560000 Public Storage Real Estate REIT - Industrial
SPG ✅ ❌ ❌ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ❌ ❌ ✅ ✅ ✅ ❌ ✅ 55.560000 Simon Property Group, Inc. Real Estate REIT - Retail
BAC ✅ ✅ ✅ ❌ ✅ ✅ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ✅ ❌ ✅ 50.000000 Bank of America Corporation Financial Services Banks - Diversified
QSR ❌ ❌ ❌ ❌ ❌ ✅ ✅ ✅ ❌ ❌ ❌ ✅ ✅ ✅ ✅ ✅ ❌ ✅ 50.000000 Restaurant Brands International Inc. Consumer Cyclical Restaurants
DIS ❌ ✅ ✅ ❌ ✅ ✅ ❌ ✅ ❌ ❌ ❌ ✅ ❌ ✅ ✅ ❌ ❌ ❌ 44.440000 The Walt Disney Company Communication Services Entertainment
HD ❌ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ❌ 44.440000 The Home Depot, Inc. Consumer Cyclical Home Improvement Retail
NU ✅ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ✅ ✅ ✅ 44.440000 Nu Holdings Ltd. Financial Services Banks - Regional
UNH ❌ ✅ ❌ ❌ ✅ ✅ ✅ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ✅ ❌ ❌ 38.890000 UnitedHealth Group Incorporated Healthcare Healthcare Plans
CRWD ✅ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ✅ ✅ ❌ 38.890000 CrowdStrike Holdings, Inc. Technology Software - Infrastructure
LMT ❌ ❌ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ✅ ❌ ❌ 38.890000 Lockheed Martin Corporation Industrials Aerospace & Defense
VGT ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard Information Technology Index Fund ETF Shares ETF, others ETF, others
SOXX ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 iShares Semiconductor ETF ETF, others ETF, others
TSLA ✅ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ❌ ✅ ❌ ✅ ❌ ❌ 33.330000 Tesla, Inc. Consumer Cyclical Auto Manufacturers
VYM ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard High Dividend Yield Index Fund ETF Shares ETF, others ETF, others
SPYG ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 State Street SPDR Portfolio S&P 500 Growth ETF ETF, others ETF, others
SOFI ✅ ❌ ❌ ❌ ✅ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ 33.330000 SoFi Technologies, Inc. Financial Services Credit Services
SPYD ❌ ✅ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 State Street SPDR Portfolio S&P 500 High Dividend ETF ETF, others ETF, others
GLD ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 SPDR Gold Shares ETF, others ETF, others
VOOV ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard S&P 500 Value Index Fund ETF Shares ETF, others ETF, others
QQQM ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Invesco NASDAQ 100 ETF ETF, others ETF, others
VOOG ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard S&P 500 Growth Index Fund ETF Shares ETF, others ETF, others
VUG ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard Growth Index Fund ETF Shares ETF, others ETF, others
VTV ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard Value Index Fund ETF Shares ETF, others ETF, others
VOO ✅ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 33.330000 Vanguard S&P 500 ETF ETF, others ETF, others
QYLD ❌ ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ✅ ❌ ❌ 27.780000 Global X NASDAQ 100 Covered Call ETF ETF, others ETF, others
SERV ❌ ❌ ❌ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ❌ ✅ ✅ ❌ 27.780000 Serve Robotics Inc. Industrials Specialty Industrial Machinery
In [8]:
# Sectors and Industries
functions.print_sector_industry(fa_df)
No description has been provided for this image
In [9]:
# --- SCORE vs BETA by Sector ---
functions.plot_risk_reward(fa_df, ratio_criteria)
No description has been provided for this image
In [10]:
# --- TOP N assets in all sectors ---
top_N_data = functions.get_top_N_assets(fa_df, ratio_criteria, top_n=10)
functions.plot_sector_treemap(top_N_data)

# Grouping the top 5 to see the distribution
summary = top_N_data.groupby(['Sector', 'Industry', 'Name', 'Ticker'])[['Score %']].max().sort_values('Score %', ascending=False)
display(summary.style.background_gradient(cmap='Greens'))
        Score %
Sector Industry Name Ticker  
Communication Services Internet Content & Information Alphabet Inc. GOOG 83.330000
Technology Semiconductors Taiwan Semiconductor Manufacturing Company Limited TSM 83.330000
NVIDIA Corporation NVDA 77.780000
Communication Services Internet Content & Information Alphabet Inc. GOOGL 77.780000
Industrials Airports & Air Services Grupo Aeroportuario del Centro Norte, S.A.B. de C.V. OMAB 77.780000
Communication Services Internet Content & Information Meta Platforms, Inc. META 77.780000
Entertainment Netflix, Inc. NFLX 72.220000
Financial Services Insurance - Diversified Berkshire Hathaway Inc. BRK-B 72.220000
Technology Semiconductor Equipment & Materials ASML Holding N.V. ASML 72.220000
Software - Infrastructure Microsoft Corporation MSFT 72.220000
In [11]:
# TOP N BY SECTOR AND INDUSTRY

functions.top_N_sector_industry(fa_df, ratio_criteria, n_per_sector=10)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [12]:
# TOP 1 BY SECTOR

final_picks = functions.top_1_sector(fa_df, ratio_criteria)
print("\n--- FINAL BEST-IN-CLASS PORTFOLIO SELECTION ---")
display(final_picks.style.hide(axis="index").background_gradient(subset=['Score %'], cmap='RdYlGn'))
--- FINAL BEST-IN-CLASS PORTFOLIO SELECTION ---
Sector Industry Ticker Score %
Communication Services Internet Content & Information GOOG 83.330000
Technology Semiconductors TSM 83.330000
Industrials Airports & Air Services OMAB 77.780000
Financial Services Insurance - Diversified BRK-B 72.220000
Consumer Defensive Beverages - Non-Alcoholic KO 66.670000
Consumer Cyclical Restaurants MCD 66.670000
Energy Oil & Gas Integrated PBR 61.110000
Real Estate REIT - Industrial PSA 55.560000
Healthcare Healthcare Plans UNH 38.890000
ETF, others ETF, others VGT 33.330000
In [13]:
#  Portfolio Summary - Beta and Scores

# Option A: All Assets
stats_all = functions.get_portfolio_summary(fa_df, ratio_criteria, mode="all")
display(stats_all.style.hide(axis="index"))

# Option B: Top 10 Overall
stats_top10 = functions.get_portfolio_summary(fa_df, ratio_criteria, mode="top_n", top_n=10)
display(stats_top10.style.hide(axis="index"))

# Option C: Top 1 per Sector
stats_sector = functions.get_portfolio_summary(fa_df, ratio_criteria, mode="sector_best")
display(stats_sector.style.hide(axis="index"))
--- PORTFOLIO SUMMARY: ALL TICKERS ---
Tickers Included (45): GOOG, TSM, META, OMAB, GOOGL, NVDA, BRK-B, ASML, MSFT, NFLX, ADBE, KO, MCD, AAPL, CAT, CME, PBR, MAR, PLTR, PSA, SPG, BAC, QSR, DIS, HD, NU, UNH, CRWD, LMT, VGT, SOXX, TSLA, VYM, SPYG, SOFI, SPYD, GLD, VOOV, QQQM, VOOG, VUG, VTV, VOO, QYLD, SERV
Metric Value
Selection Strategy ALL TICKERS
Total Tickers 45
Average Match Score 51.73%
Average Beta 0.75
Portfolio Character CONSERVATIVE / DEFENSIVE
--- PORTFOLIO SUMMARY: TOP 10 OVERALL ---
Tickers Included (10): GOOG, TSM, META, OMAB, GOOGL, NVDA, BRK-B, ASML, MSFT, NFLX
Metric Value
Selection Strategy TOP 10 OVERALL
Total Tickers 10
Average Match Score 76.67%
Average Beta 1.25
Portfolio Character AGGRESSIVE / HIGH VOLATILITY
--- PORTFOLIO SUMMARY: TOP 1 PER SECTOR ---
Tickers Included (10): GOOG, TSM, OMAB, BRK-B, KO, MCD, PBR, PSA, UNH, VGT
Metric Value
Selection Strategy TOP 1 PER SECTOR
Total Tickers 10
Average Match Score 63.89%
Average Beta 0.60
Portfolio Character CONSERVATIVE / DEFENSIVE

Fundamental Analysis ONE Ticker¶

TS302_Stock Full Analysis.ipynb

This section is to see in more detail any particular ticker

In [14]:
# 1. Create the widget
ticker_dropdown = widgets.Dropdown(
    options=tickers,
    #options=["KOF", "AAPL", "MSFT", "MA","NVDA", "GOOGL", "AMZN", "META", "TSM","BRK-B", "V", "JPM", "XOM", "LLY", "MRK", "UNH", "PG", "MA","CVX", "KO", "PEP", "COST", "TMO", "ORCL", "CSCO", "NKE", "VZ", "ASML", "TXN", "ABT", "TM", "SAP", "AMD", "NFLX", "NOW", "ADBE", "LVMUY", "BABA", "SHEL", "TMUS", "QCOM", "PFE", "SNY", "AZN", "TOT", "GSK", "RIO", "BHP", "MCD​"],
    #options=["BTC-USD", "ETH-USD", "USDT-USD", "XRP-USD", "LTC-USD", "ADA-USD", "DOT-USD", "BCH-USD", "XLM-USD", "LINK-USD"]
    value=tickers[0],  # Default selected value (must be from the options)
    description='Select Ticker:',
    disabled=False,
)

def on_change(selected_ticker):
    clear_output(wait=False)
    display(fa_df.loc[selected_ticker])
    
    global ticker_widget
    ticker_widget = selected_ticker

    return ticker_widget

# # 4. Link the function to the widget and capture the output
interactive_plot = widgets.interactive_output(on_change, {'selected_ticker': ticker_dropdown})

# # 5. Display the widget and the output area in your notebook cell
display(ticker_dropdown, interactive_plot)
Dropdown(description='Select Ticker:', options=('QYLD', 'NVDA', 'PBR', 'PLTR', 'MSFT', 'AAPL', 'NU', 'DIS', 'V…
Output()
In [15]:
# Choose any ticket from the list above
ticker = yf.Ticker(ticker_widget)
ticker_name = ticker.info.get('symbol')
print(ticker_name)
QYLD

History

In [16]:
# Example of history() of any ONE ticker for "1y"
hist = ticker.history(period="1y", auto_adjust=True)
print(ticker_name)
display(hist)
QYLD
Open High Low Close Volume Dividends Stock Splits Capital Gains
Date
2025-01-06 00:00:00-05:00 16.399330 16.434827 16.372709 16.399330 4535000 0.0 0.0 0.0
2025-01-07 00:00:00-05:00 16.443700 16.443700 16.248469 16.283966 8486600 0.0 0.0 0.0
2025-01-08 00:00:00-05:00 16.297274 16.328334 16.195223 16.310585 11652700 0.0 0.0 0.0
2025-01-10 00:00:00-05:00 16.275095 16.283969 16.062117 16.159731 15684100 0.0 0.0 0.0
2025-01-13 00:00:00-05:00 16.017742 16.133106 15.955624 16.133106 8383300 0.0 0.0 0.0
... ... ... ... ... ... ... ... ...
2025-12-29 00:00:00-05:00 17.719999 17.750000 17.709999 17.740000 4555200 0.0 0.0 0.0
2025-12-30 00:00:00-05:00 17.730000 17.760000 17.725000 17.730000 4812700 0.0 0.0 0.0
2025-12-31 00:00:00-05:00 17.730000 17.740000 17.670000 17.670000 4961300 0.0 0.0 0.0
2026-01-02 00:00:00-05:00 17.750000 17.780001 17.620001 17.680000 10828500 0.0 0.0 0.0
2026-01-05 00:00:00-05:00 17.725000 17.789000 17.719999 17.760000 5833600 0.0 0.0 0.0

250 rows × 8 columns

  • Info
  • Income Statement
  • Balance Sheet
In [17]:
# info
ticker_info = ticker.info
# Optional: Print in JSON format all info
#print(json.dumps(ticker_info, indent=4))

#Income Statement
ticker_income_stmt = ticker.income_stmt
# Optional: Print Income Statement
#print(ticker_income_stmt)

#Balance Sheet
ticker_balance_sheet = ticker.balance_sheet
# Optional: Print Balance Sheet
#print(ticker_balance_sheet)


# To-Do: Support the Stock selection based on Ratios using the Financial Statements
In [18]:
# Print info by category (some selected data only)

def print_info_by_category(info_list, name):
    print(f"\n{name}")
    for key in info_list:
        try:
            value = ticker_info.get(key, 'N/A')
            print(f"{key}: {value}")
        except Exception as e:
            print(f"Error retrieving '{key} for {ticker_name}: {e}")

# One can choose which info parameters to print in each category:
basic_info = ['symbol', 'longName', 'sector', 'industry', 'country']
market_info = ['currentPrice', 'marketCap', 'volume', '52WeekChange', 'fiftyTwoWeekHigh', 'fiftyTwoWeekLow']
financial_info = ['priceToBook', 'forwardPE', 'trailingPE', 'profitMargins', 'totalRevenue', 'debtToEquity',
                  'epsForward','ebitda','floatShares','forwardEps','grossMargins','grossProfits','operatingCashflow',
                  'operatingMargins','returnOnAssets','returnOnEquity','revenueGrowth','revenuePerShare','impliedSharesOutstanding',
                  'totalCash','totalDebt']
dividends_info = ['dividendYield','payoutRatio','dividendRate']
shares_info = ['heldPercentInsiders','heldPercentInstitutions']
technical_info = ['sharesOutstanding','beta','currency']

print_info_by_category(basic_info,      "1. Basic info:")
print_info_by_category(market_info,     "2. Market info:")
print_info_by_category(financial_info,  "3. Financial info:")
print_info_by_category(dividends_info,  "4. Dividends info:")
print_info_by_category(shares_info,     "5. Shares management info:")
print_info_by_category(technical_info,  "6. Technical info:")

# Market Cap = Current Share Price × Shares Outstanding
1. Basic info:
symbol: QYLD
longName: Global X NASDAQ 100 Covered Call ETF
sector: N/A
industry: N/A
country: N/A

2. Market info:
currentPrice: N/A
marketCap: N/A
volume: 5818312
52WeekChange: N/A
fiftyTwoWeekHigh: 18.89
fiftyTwoWeekLow: 14.475

3. Financial info:
priceToBook: N/A
forwardPE: N/A
trailingPE: 34.672573
profitMargins: N/A
totalRevenue: N/A
debtToEquity: N/A
epsForward: N/A
ebitda: N/A
floatShares: N/A
forwardEps: N/A
grossMargins: N/A
grossProfits: N/A
operatingCashflow: N/A
operatingMargins: N/A
returnOnAssets: N/A
returnOnEquity: N/A
revenueGrowth: N/A
revenuePerShare: N/A
impliedSharesOutstanding: N/A
totalCash: N/A
totalDebt: N/A

4. Dividends info:
dividendYield: 10.46
payoutRatio: N/A
dividendRate: N/A

5. Shares management info:
heldPercentInsiders: N/A
heldPercentInstitutions: N/A

6. Technical info:
sharesOutstanding: N/A
beta: N/A
currency: USD

TO-DO: Monitorear Recompra o dilucion de acciones. Con base en el numero de acciones disponibles por anio

Dividends, Splits and Recommendations

In [19]:
# 7. Other info:
print("\n7. Other info:")      
other_info = {'Dividends' : ticker.dividends, 
              'Splits' : ticker.splits, 
              'Recommendations' : ticker.recommendations
            #   'Recommendations Summary' : ticker.recommendations_summary
              }

functions.print_dividends_splits_recommendations(other_info, ticker_name)
7. Other info:
HTTP Error 404: 
Dividends:
No description has been provided for this image
Dividends:
Date
2014-01-22 00:00:00-05:00    0.257
2014-02-26 00:00:00-05:00    0.193
2014-03-26 00:00:00-04:00    0.193
2014-04-23 00:00:00-04:00    0.263
2014-05-21 00:00:00-04:00    0.245
                             ...  
2025-08-18 00:00:00-04:00    0.168
2025-09-22 00:00:00-04:00    0.170
2025-10-20 00:00:00-04:00    0.173
2025-11-24 00:00:00-05:00    0.173
2025-12-22 00:00:00-05:00    0.178
Name: Dividends, Length: 139, dtype: float64
There are not Splits in this period for 'QYLD'
There are not Recommendations in this period for 'QYLD'
In [20]:
import operator
from dataclasses import dataclass
from typing import Callable, Any, Optional

RetrievalFunc = Callable[[dict], Any]   #ticker.info : is a dictionary: ticker.info.get('trailingPE', 'N/A'). Look for ticker_info =  ticker.info above
CompareFunc = Callable[[Any, Any], bool]

@dataclass
class FinancialRule:
    retrieval_func: RetrievalFunc   # function used to extract the specific metric from the data source
    metric_name: str      
    target_value: Optional[Any] = None
    comparison_func: Optional[Callable[[Any, Any], bool]] = None
          # for printing pruposes
    # target_value: float             # The target value to compare against
    # comparison_func: CompareFunc    # comparison operator function (e.g., operator.lt for <)


# financial_rules = {
#     'D/E_%': FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('debtToEquity', 'N/A'), target_value=100.0, comparison_func=operator.gt, metric_name='D/E_%')
# }

financial_rules = {
    'Name':                     FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('longName', 'N/A'),            metric_name='Name'),
    'Sector':                   FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('sector', 'ETF, others'),      metric_name='Sector'),
    'Industry':                 FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('industry', 'ETF, others'),    metric_name='Industry'),
    'CAGR_%':                   FinancialRule(retrieval_func=lambda ticker_info: functions.get_cagr(ticker_info.get('symbol'), start_date, today),   target_value=10.0, comparison_func=operator.ge, metric_name='CAGR_%'),
    'P/E Ratio':                FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('trailingPE', 'N/A'),          target_value=25.0, comparison_func=operator.lt, metric_name='P/E Ratio'),
    'P/B Ratio':                FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('priceToBook', 'N/A'),         target_value=2.0, comparison_func=operator.lt, metric_name='P/B Ratio'),
    'ROIC_%':                   FinancialRule(retrieval_func=lambda ticker_info: functions.get_roic(ticker_info.get('symbol')), target_value=15.0, comparison_func=operator.gt, metric_name='ROIC_%'),
    'D/E_%':                    FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('debtToEquity', 'N/A'),        target_value=100.0, comparison_func=operator.lt, metric_name='D/E_%'),
    'EPS_usd':                  FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('epsForward', 'N/A'),          target_value=0.0, comparison_func=operator.gt, metric_name='EPS_usd'),
    'ROE Ratio':                FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('returnOnEquity', 'N/A'),      target_value=0.15, comparison_func=operator.ge, metric_name='ROE Ratio'),
    'EBIT Margin_%':            FinancialRule(retrieval_func=lambda ticker_info: functions.get_ebit_margin(ticker_info.get('symbol')),  target_value=10.0, comparison_func=operator.ge, metric_name='EBIT Margin_%'),
    'Gross Margin_ratio':       FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('grossMargins', 'N/A'),        target_value=0.40, comparison_func=operator.ge, metric_name='Gross Margin_ratio'),
    'Net Margin_%':             FinancialRule(retrieval_func=lambda ticker_info: functions.get_net_margin(ticker_info.get('symbol')),   target_value=15.0, comparison_func=operator.ge, metric_name='Net Margin_%'),
    'Current Ratio':            FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('currentRatio', 'N/A'),        target_value=1.5, comparison_func=operator.ge, metric_name='Current Ratio'),
    'Overall Risk':             FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('overallRisk', 'N/A'),         target_value=5.0, comparison_func=operator.lt, metric_name='Overall Risk'),
    'Beta':                     FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('beta', 'N/A'),                target_value=1.0, comparison_func=operator.eq, metric_name='Beta'),
    'EBITDA_usd':               FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('ebitda', 'N/A'),              target_value=0.0, comparison_func=operator.gt, metric_name='EBITDA_usd'),
    'EBITDA Margins Ratio':     FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('ebitdaMargins', 'N/A'),       target_value=0.15, comparison_func=operator.gt, metric_name='EBITDA Margins Ratio'),
    'Earning Growth Ratio':     FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('earningsGrowth', 'N/A'),      target_value=0.15, comparison_func=operator.gt, metric_name='Earning Growth Ratio'),
    'Revenue Growth Ratio':     FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('revenueGrowth', 'N/A'),       target_value=0.15, comparison_func=operator.gt, metric_name='Revenue Growth Ratio'),
    'Operating Margins Ratio':  FinancialRule(retrieval_func=lambda ticker_info: ticker_info.get('operatingMargins', 'N/A'),    target_value=0.15, comparison_func=operator.gt, metric_name='Operating Margins Ratio'),
}

def evaluate_ticker_rules(financial_rules: dict, ticker_info: dict):
    print(f"---Evaluating Rules for {ticker_info.get('symbol', 'Unknown Ticker')}---")

    if not ticker_info.get('symbol'):
        print("Status: ⚠️ SKIP (No ticker data available)")
        return       

    for rule_name, rule in financial_rules.items():
        # 1. Call the stored retrieval function to get the actual metric value
        actual_value = rule.retrieval_func(ticker_info)
        
        print(f"\nRule: {rule_name}")
        print(f"Metric: {rule.metric_name}, Value found: {actual_value}")


        if rule.comparison_func is not None:
            if actual_value is None or actual_value == 'N/A':
                print("Status: ⚠️ SKIP (Data for comparison not available)")
                continue

            # 2. Call the stored comparison function
            is_pass = rule.comparison_func(actual_value, rule.target_value)
            
            if is_pass:
                print(f"Status: ✅ PASS ({actual_value} is acceptable)")
            else:
                print(f"Status: ❌ FAIL ({actual_value} fails rule to be {rule.comparison_func.__name__} {rule.target_value})")

        else:
            print("Status: ✅ INFO (No comparison needed)")

evaluate_ticker_rules(financial_rules, ticker_info)
---Evaluating Rules for QYLD---

Rule: Name
Metric: Name, Value found: Global X NASDAQ 100 Covered Call ETF
Status: ✅ INFO (No comparison needed)

Rule: Sector
Metric: Sector, Value found: ETF, others
Status: ✅ INFO (No comparison needed)

Rule: Industry
Metric: Industry, Value found: ETF, others
Status: ✅ INFO (No comparison needed)

Rule: CAGR_%
Metric: CAGR_%, Value found: 7.28
Status: ❌ FAIL (7.28 fails rule to be ge 10.0)

Rule: P/E Ratio
Metric: P/E Ratio, Value found: 34.672573
Status: ❌ FAIL (34.672573 fails rule to be lt 25.0)

Rule: P/B Ratio
Metric: P/B Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)
Error (get_roic): Financial data for 'QYLD' not available or incomplete.

Rule: ROIC_%
Metric: ROIC_%, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: D/E_%
Metric: D/E_%, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: EPS_usd
Metric: EPS_usd, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: ROE Ratio
Metric: ROE Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)
(get_ebit_margin) No annual income statement found for 'QYLD'.

Rule: EBIT Margin_%
Metric: EBIT Margin_%, Value found: None
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Gross Margin_ratio
Metric: Gross Margin_ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)
Error (get_net_margin): Financial data for 'QYLD' not available.

Rule: Net Margin_%
Metric: Net Margin_%, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Current Ratio
Metric: Current Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Overall Risk
Metric: Overall Risk, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Beta
Metric: Beta, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: EBITDA_usd
Metric: EBITDA_usd, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: EBITDA Margins Ratio
Metric: EBITDA Margins Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Earning Growth Ratio
Metric: Earning Growth Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Revenue Growth Ratio
Metric: Revenue Growth Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

Rule: Operating Margins Ratio
Metric: Operating Margins Ratio, Value found: N/A
Status: ⚠️ SKIP (Data for comparison not available)

All Tickers in Portfolio¶

Import Prices from yfinance (All Tickers)¶

TS302_Stock Full Analysis.ipynb

In [21]:
# import data from yahoo Finance
print(f"Number of days since Brokerage account was opened: {no_days}")
df = yf.download(tickers, start=start_date, end=today, auto_adjust=True)
df.info()
[***                    7%                       ]  3 of 45 completed
Number of days since Brokerage account was opened: 1717 days, 0:00:00
[*********************100%***********************]  45 of 45 completed
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1180 entries, 2021-04-26 to 2026-01-05
Columns: 225 entries, ('Close', 'AAPL') to ('Volume', 'VYM')
dtypes: float64(182), int64(43)
memory usage: 2.0 MB

Prices (Close)¶

In [22]:
prices_raw = df["Close"]
print('-- Last 5 days of prices --')
display(prices_raw.tail(5))
-- Last 5 days of prices --
Ticker AAPL ADBE ASML BAC BRK-B CAT CME CRWD DIS GLD ... TSLA TSM UNH VGT VOO VOOG VOOV VTV VUG VYM
Date
2025-12-29 273.760010 353.160004 1066.000000 55.349998 501.049988 578.609985 278.420013 475.910004 114.190002 398.600006 ... 459.640015 300.920013 328.940002 763.099976 632.599976 448.600006 206.630005 192.589996 492.540009 144.699997
2025-12-30 273.079987 352.510010 1072.140015 55.279999 503.709991 577.390015 275.829987 475.630005 114.790001 398.890015 ... 454.429993 299.579987 332.160004 760.890015 631.719971 447.769989 206.410004 192.369995 491.690002 144.550003
2025-12-31 271.859985 349.989990 1069.859985 55.000000 502.649994 572.869995 273.079987 468.760010 113.769997 396.309998 ... 449.720001 303.890015 330.109985 753.780029 627.130005 444.589996 204.850006 190.990005 487.859985 143.520004
2026-01-02 271.010010 333.299988 1163.780029 55.950001 496.850006 598.409973 269.679993 453.579987 111.849998 398.279999 ... 438.070007 319.609985 336.399994 755.979980 628.299988 444.850006 205.610001 192.809998 486.200012 144.759995
2026-01-05 267.260010 331.559998 1228.189941 56.889999 498.519989 616.099976 275.059998 456.549988 114.070000 408.760010 ... 451.670013 322.250000 342.019989 757.419983 632.460022 446.510010 207.509995 194.649994 488.450012 145.820007

5 rows × 45 columns

In [23]:
# drop NaN (days without price)
prices = prices_raw.dropna()
display(prices.tail(5))
Ticker AAPL ADBE ASML BAC BRK-B CAT CME CRWD DIS GLD ... TSLA TSM UNH VGT VOO VOOG VOOV VTV VUG VYM
Date
2025-12-29 273.760010 353.160004 1066.000000 55.349998 501.049988 578.609985 278.420013 475.910004 114.190002 398.600006 ... 459.640015 300.920013 328.940002 763.099976 632.599976 448.600006 206.630005 192.589996 492.540009 144.699997
2025-12-30 273.079987 352.510010 1072.140015 55.279999 503.709991 577.390015 275.829987 475.630005 114.790001 398.890015 ... 454.429993 299.579987 332.160004 760.890015 631.719971 447.769989 206.410004 192.369995 491.690002 144.550003
2025-12-31 271.859985 349.989990 1069.859985 55.000000 502.649994 572.869995 273.079987 468.760010 113.769997 396.309998 ... 449.720001 303.890015 330.109985 753.780029 627.130005 444.589996 204.850006 190.990005 487.859985 143.520004
2026-01-02 271.010010 333.299988 1163.780029 55.950001 496.850006 598.409973 269.679993 453.579987 111.849998 398.279999 ... 438.070007 319.609985 336.399994 755.979980 628.299988 444.850006 205.610001 192.809998 486.200012 144.759995
2026-01-05 267.260010 331.559998 1228.189941 56.889999 498.519989 616.099976 275.059998 456.549988 114.070000 408.760010 ... 451.670013 322.250000 342.019989 757.419983 632.460022 446.510010 207.509995 194.649994 488.450012 145.820007

5 rows × 45 columns

In [24]:
print(f"prices_raw shape: {prices_raw.shape}")
print(f"prices shape: {prices.shape}")

if len(prices) < len(prices_raw):
    print(f"Reduction of {(len(prices_raw) - len(prices))}")
    print(f"It can be noticed that the amount of rows in the dataframe dropped from {len(prices_raw)} to {len(prices)} after the drop.na() operation. This is because there are a couple of assets that are relatively new in the public market (post IPO).")
prices_raw shape: (1180, 45)
prices shape: (458, 45)
Reduction of 722
It can be noticed that the amount of rows in the dataframe dropped from 1180 to 458 after the drop.na() operation. This is because there are a couple of assets that are relatively new in the public market (post IPO).
In [25]:
print(f"Reminder: the original 'start_date' was: {start_date.date().strftime("%B %d, %Y")}, when the original portfolio was created.")

df_ = df['Close']
first_valid_dates = df_.apply(pd.Series.first_valid_index)
first_valid_dates.name = "First Valid Date"
first_valid_dates = first_valid_dates.sort_values(ascending=False)


print(f"\nThe asset with the smallest amount of data available is: '{first_valid_dates.index[0]}' starting from '{first_valid_dates.iloc[0].date().strftime("%B %d, %Y")}' only.")

print("\nOldest available dates for each asset:")
display(first_valid_dates)
Reminder: the original 'start_date' was: April 25, 2021, when the original portfolio was created.

The asset with the smallest amount of data available is: 'SERV' starting from 'March 08, 2024' only.

Oldest available dates for each asset:
Ticker
SERV    2024-03-08
NU      2021-12-09
AAPL    2021-04-26
SPYD    2021-04-26
PSA     2021-04-26
QQQM    2021-04-26
QSR     2021-04-26
QYLD    2021-04-26
SOFI    2021-04-26
SOXX    2021-04-26
SPG     2021-04-26
SPYG    2021-04-26
PBR     2021-04-26
TSLA    2021-04-26
TSM     2021-04-26
UNH     2021-04-26
VGT     2021-04-26
VOO     2021-04-26
VOOG    2021-04-26
VOOV    2021-04-26
VTV     2021-04-26
VUG     2021-04-26
PLTR    2021-04-26
OMAB    2021-04-26
ADBE    2021-04-26
GOOG    2021-04-26
ASML    2021-04-26
BAC     2021-04-26
BRK-B   2021-04-26
CAT     2021-04-26
CME     2021-04-26
CRWD    2021-04-26
DIS     2021-04-26
GLD     2021-04-26
GOOGL   2021-04-26
NVDA    2021-04-26
HD      2021-04-26
KO      2021-04-26
LMT     2021-04-26
MAR     2021-04-26
MCD     2021-04-26
META    2021-04-26
MSFT    2021-04-26
NFLX    2021-04-26
VYM     2021-04-26
Name: First Valid Date, dtype: datetime64[ns]
In [26]:
# -- PLOT ASSET PRICES --
functions.plot_prices(prices=prices, yaxis_label="Price (USD)")

Stats (describe)¶

In [27]:
# statistics about prices
prices.describe()
Out[27]:
Ticker AAPL ADBE ASML BAC BRK-B CAT CME CRWD DIS GLD ... TSLA TSM UNH VGT VOO VOOG VOOV VTV VUG VYM
count 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 ... 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000 458.000000
mean 222.552679 437.168252 835.958809 43.304069 467.779913 389.081520 236.470775 387.847784 105.330561 277.980262 ... 303.332074 201.119318 440.994565 617.778433 536.028584 365.229192 184.176589 169.630523 406.587243 126.395603
std 28.227665 76.543920 137.833934 5.724365 36.503627 78.722380 32.720417 82.221034 10.323902 56.767730 ... 94.636566 49.471145 104.222733 81.988133 50.927276 46.526895 10.670231 11.152038 49.141947 9.622135
min 163.664917 312.399994 590.981628 33.286674 396.730011 270.854095 183.360397 217.889999 80.826134 199.710007 ... 142.050003 124.747917 234.701340 468.845581 445.162018 283.491333 160.399734 148.670639 319.818878 109.263657
25% 204.992336 361.214996 720.596664 38.356688 445.197495 337.269882 203.922478 320.304993 96.814312 230.795002 ... 221.462502 165.846745 331.912491 556.428314 496.731300 331.431778 175.030312 161.032127 370.120850 118.651709
50% 223.477219 438.600006 797.170013 43.228786 472.275009 358.955170 237.254982 377.800003 109.514038 263.349991 ... 302.714996 190.106636 473.674393 605.396118 531.847992 360.575485 183.908020 169.886086 403.221786 126.483810
75% 238.251236 502.427505 954.881851 46.758083 496.355011 411.348167 268.550484 461.902496 112.741642 309.809990 ... 389.190002 231.559891 530.617584 682.324539 574.526367 405.175117 191.088543 176.452358 448.428337 133.308155
max 286.190002 586.549988 1228.189941 56.889999 539.799988 625.609985 286.685730 557.530029 123.176605 416.739990 ... 489.880005 322.250000 607.890625 800.707397 634.840027 454.862305 207.509995 194.649994 503.744995 146.816788

8 rows × 45 columns

Normalized Prices¶

Normalize to 100

Normalizar precios de diferentes magnitudes dividiendo entre el primer registro. Todos los precios parten del mismo punto que es el 100.

$$ \frac {P_t}{P_0} * 100 $$

Nota: esto ya no es mas el Precio al Cierre sino un indice de crecimiento en el tiempo.

In [28]:
# Normalized Prices

prices_normalized = (prices / prices.iloc[0]) * 100


# -- PLOT ASSET PRICES --
functions.plot_prices(prices=prices_normalized, yaxis_label="Price (USD)")

Daily Returns¶

TS303_yfinance Indices Bursatiles.ipynb

Normal (simple or arithmetic) returns:

$$Return(R_t) = \frac{P_t - P_{t-1}}{P_{t-1}} = \frac {P_t}{P_{t-1}} -1 $$

In [29]:
# Daily Returns. Using pct_change()
# Normal (simple or arithmetic) returns
# Expresed in FRACTION. 
# If Percentage is needed then multiply by 100.
daily_returns = prices.pct_change(fill_method=None)
daily_returns = daily_returns.dropna()
daily_returns.tail(5)
Out[29]:
Ticker AAPL ADBE ASML BAC BRK-B CAT CME CRWD DIS GLD ... TSLA TSM UNH VGT VOO VOOG VOOV VTV VUG VYM
Date
2025-12-29 0.001317 -0.001809 -0.006292 -0.014599 0.005519 -0.007530 0.006107 -0.010973 0.005548 -0.043528 ... -0.032724 -0.006340 -0.008709 -0.005305 -0.003529 -0.004637 -0.001401 -0.000986 -0.005090 -0.002688
2025-12-30 -0.002484 -0.001841 0.005760 -0.001265 0.005309 -0.002108 -0.009303 -0.000588 0.005254 0.000728 ... -0.011335 -0.004453 0.009789 -0.002896 -0.001391 -0.001850 -0.001065 -0.001142 -0.001726 -0.001037
2025-12-31 -0.004468 -0.007149 -0.002127 -0.005065 -0.002104 -0.007828 -0.009970 -0.014444 -0.008886 -0.006468 ... -0.010365 0.014387 -0.006172 -0.009344 -0.007266 -0.007102 -0.007558 -0.007174 -0.007789 -0.007126
2026-01-02 -0.003127 -0.047687 0.087787 0.017273 -0.011539 0.044583 -0.012451 -0.032383 -0.016876 0.004971 ... -0.025905 0.051729 0.019054 0.002919 0.001866 0.000585 0.003710 0.009529 -0.003403 0.008640
2026-01-05 -0.013837 -0.005220 0.055345 0.016801 0.003361 0.029562 0.019950 0.006548 0.019848 0.026313 ... 0.031045 0.008260 0.016706 0.001905 0.006621 0.003732 0.009241 0.009543 0.004628 0.007323

5 rows × 45 columns

In [30]:
results_df = functions.plot_returns_distributions(daily_returns, num_cols=4)
No description has been provided for this image
================================================================================
FAT-TAIL RISK RANKING (Sorted by Highest Kurtosis)
================================================================================
  Mean Median Std Dev Skewness Kurtosis
Ticker          
SERV 0.0061 -0.0034 0.1318 5.9456 90.6288
QYLD 0.0005 0.0006 0.0094 2.0870 42.2459
UNH -0.0003 0.0006 0.0252 -2.2992 20.7657
VOOV 0.0005 0.0006 0.0086 0.4293 18.1899
VOO 0.0008 0.0011 0.0102 0.6920 17.8392
ADBE -0.0009 -0.0001 0.0214 -1.1348 15.0829
QQQM 0.0009 0.0016 0.0133 0.7721 14.1523
VTV 0.0006 0.0005 0.0084 -0.0227 14.0826
VUG 0.0009 0.0014 0.0133 0.7047 14.0708
VYM 0.0006 0.0007 0.0086 0.0657 13.7854
AAPL 0.0012 0.0016 0.0180 0.9798 13.5882
DIS 0.0003 -0.0001 0.0171 0.2075 13.1759
SPYG 0.0010 0.0017 0.0132 0.6352 13.0041
VOOG 0.0010 0.0016 0.0132 0.5869 12.9678
LMT 0.0006 0.0009 0.0144 -1.7608 12.1996
VGT 0.0010 0.0022 0.0160 0.5416 11.9247
SOXX 0.0010 0.0012 0.0238 0.4233 9.1252
BAC 0.0012 0.0014 0.0159 -0.6499 8.7776
SPG 0.0007 0.0014 0.0149 -0.5224 8.4795
META 0.0008 0.0009 0.0221 0.2239 8.3067
MSFT 0.0005 0.0011 0.0142 0.5917 8.2515
BRK-B 0.0005 0.0008 0.0107 -0.2961 7.6443
PLTR 0.0049 0.0030 0.0398 0.8398 6.5827
NU 0.0014 0.0019 0.0270 -0.7662 6.5808
ASML 0.0009 0.0021 0.0273 -0.3530 6.3391
CAT 0.0015 0.0009 0.0188 0.5463 6.2754
NFLX 0.0011 0.0015 0.0199 -0.1149 6.2151
SPYD 0.0004 0.0007 0.0090 -0.2973 5.6996
MAR 0.0006 0.0006 0.0163 0.3715 5.2052
CRWD 0.0012 0.0013 0.0288 0.2218 4.8889
NVDA 0.0022 0.0027 0.0319 -0.0131 4.5842
GOOG 0.0020 0.0029 0.0189 0.4433 4.5308
GOOGL 0.0021 0.0029 0.0191 0.4620 4.4202
TSLA 0.0029 0.0010 0.0408 0.6801 4.3883
PBR 0.0002 0.0007 0.0184 -0.2319 3.8937
TSM 0.0021 0.0027 0.0257 -0.0901 3.0021
GLD 0.0016 0.0020 0.0115 -0.5681 2.9093
MCD 0.0002 0.0001 0.0116 -0.1298 2.8369
OMAB 0.0015 0.0011 0.0217 -0.0996 2.6707
KO 0.0005 0.0003 0.0099 0.2686 2.4035
SOFI 0.0036 0.0047 0.0364 -0.0165 2.3834
PSA 0.0000 0.0004 0.0143 -0.2022 1.8007
CME 0.0008 0.0014 0.0110 -0.6110 1.7262
QSR -0.0001 -0.0004 0.0143 -0.2684 1.4833
HD 0.0000 -0.0010 0.0140 0.0574 1.4739
In [31]:
# -- PLOT DAILY RETURNS --
functions.plot_daily_returns(daily_returns)

Summary of Daily Returns¶

In [32]:
# stats and summary of daily retutns
print(f"Number of days of evaluation: {(today - daily_returns.index[0]).days}")
print(f"since {daily_returns.index[0].date().strftime("%B %d, %Y")} until {today.date().strftime("%B %d, %Y")}")

max_daily_return = daily_returns.describe().loc['max'].sort_values(ascending=False)
print(f"\nThe asset with the biggest Return in a single day is '{max_daily_return.index[0]}' with {100*max_daily_return.iloc[0]:,.5}%")

min_daily_return = daily_returns.describe().loc['min'].sort_values(ascending=True)
print(f"The asset with the lowest Return in a single day is '{min_daily_return.index[0]}' with {100*min_daily_return.iloc[0]:,.5}%")

average_daily_returns = daily_returns.mean()
print(f"The average of all Daily Returns is {average_daily_returns.mean()*100:,.4}%")

max_std = daily_returns.describe().loc['std'].sort_values(ascending=False)
print(f"\nThe asset with the biggest StdDev in Daily Returns is '{max_std.index[0]}' with {100*max_std.iloc[0]:,.5}%")
print(f"The asset with the smallest StdDev in Daily Returns is '{max_std.index[-1]}' with {100*max_std.iloc[-1]:,.5}%")
Number of days of evaluation: 666
since March 11, 2024 until January 06, 2026

The asset with the biggest Return in a single day is 'SERV' with 187.07%
The asset with the lowest Return in a single day is 'SERV' with -77.647%
The average of all Daily Returns is 0.1137%

The asset with the biggest StdDev in Daily Returns is 'SERV' with 13.175%
The asset with the smallest StdDev in Daily Returns is 'VTV' with 0.83946%

Annualized Returns¶

In [33]:
# Annualized Returns
annualized_return_tickers = daily_returns.mean() * 250
annualized_return_tickers.name = "Annualized Returns:"
annualized_return_tickers = annualized_return_tickers.sort_values(ascending=False)
print("Annualized Returns (%):")
print(f"{round(annualized_return_tickers, 2)* 100}")
Annualized Returns (%):
Ticker
SERV     153.0
PLTR     123.0
SOFI      90.0
TSLA      72.0
NVDA      55.0
TSM       53.0
GOOGL     51.0
GOOG      51.0
GLD       40.0
CAT       39.0
OMAB      38.0
NU        36.0
BAC       31.0
CRWD      29.0
AAPL      29.0
NFLX      28.0
SOXX      26.0
SPYG      25.0
VOOG      25.0
VGT       24.0
VUG       23.0
ASML      22.0
QQQM      22.0
META      21.0
VOO       19.0
CME       19.0
SPG       18.0
MAR       16.0
VYM       16.0
VTV       15.0
LMT       14.0
BRK-B     13.0
QYLD      13.0
VOOV      12.0
KO        12.0
MSFT      12.0
SPYD      11.0
DIS        7.0
PBR        6.0
MCD        5.0
PSA        1.0
HD         0.0
QSR       -3.0
UNH       -8.0
ADBE     -22.0
Name: Annualized Returns:, dtype: float64

Ticker
SERV     153.0
PLTR     123.0
SOFI      90.0
TSLA      72.0
NVDA      55.0
TSM       53.0
GOOGL     51.0
GOOG      51.0
GLD       40.0
CAT       39.0
OMAB      38.0
NU        36.0
BAC       31.0
CRWD      29.0
AAPL      29.0
NFLX      28.0
SOXX      26.0
SPYG      25.0
VOOG      25.0
VGT       24.0
VUG       23.0
ASML      22.0
QQQM      22.0
META      21.0
VOO       19.0
CME       19.0
SPG       18.0
MAR       16.0
VYM       16.0
VTV       15.0
LMT       14.0
BRK-B     13.0
QYLD      13.0
VOOV      12.0
KO        12.0
MSFT      12.0
SPYD      11.0
DIS        7.0
PBR        6.0
MCD        5.0
PSA        1.0
HD         0.0
QSR       -3.0
UNH       -8.0
ADBE     -22.0
Name: Annualized Returns:, dtype: float64

Cumulative Returns in Period¶

TS303_yfinance Indices Bursatiles.ipynb

In [34]:
# cumprod(): calculates the total return over a period by compounding the daily returns. (a)(ab)(abc)
# It reflects how an initial investment would grow if it were continuously reinvested and earned the daily returns.
cumulative_returns = (1 + daily_returns).cumprod()
cumulative_returns = (cumulative_returns - 1)*100
print("Cumulative Returns in %:")
cumulative_returns.tail(5)
Cumulative Returns in %:
Out[34]:
Ticker AAPL ADBE ASML BAC BRK-B CAT CME CRWD DIS GLD ... TSLA TSM UNH VGT VOO VOOG VOOV VTV VUG VYM
Date
2025-12-29 61.654779 -35.985789 8.910560 61.985575 24.283764 75.127795 38.696582 47.409012 5.601360 97.688834 ... 162.142138 111.308356 -28.452585 47.946322 37.928557 51.768926 22.272159 27.397622 46.504114 30.200598
2025-12-30 61.253226 -36.103607 9.537870 61.780716 24.943569 74.758547 37.406345 47.322285 6.156230 97.832666 ... 159.170755 110.367379 -27.752206 47.517865 37.736686 51.488117 22.141975 27.252092 46.251283 30.065634
2025-12-31 60.532818 -36.560389 9.304925 60.961281 24.680640 73.390473 36.036416 45.194363 5.212944 96.553086 ... 156.484550 113.393914 -28.198104 46.139413 36.735916 50.412272 21.218855 26.339234 45.112059 29.138844
2026-01-02 60.030909 -39.585639 18.900502 63.741523 23.241973 81.120654 34.342689 40.492482 3.437356 97.530124 ... 149.840320 124.432598 -26.829970 46.565930 36.991012 50.500238 21.668577 27.543152 44.618307 30.254584
2026-01-05 57.816541 -39.901032 25.481102 66.492494 23.656207 86.474885 37.022770 41.412414 5.490383 102.727769 ... 157.596682 126.286437 -25.607571 46.845111 37.898043 51.061845 22.792888 28.760304 45.287560 31.208380

5 rows × 45 columns

In [35]:
# -- PLOT CUMULATIVE RETURNS --
functions.plot_cumulative_returns(cumulative_returns)

Final Cumulated Returns in Period¶

In [36]:
# Rendimientos Acumulados al Final del Tiempo en (%) use .prod() y -1
print(f"Final Cumulative Returns in (%) in {(today - daily_returns.index[0]).days} days of evaluation: ")
print(f"since {daily_returns.index[0].date().strftime("%B %d, %Y")} until {today.date().strftime("%B %d, %Y")}")

cumulative_returns_final = cumulative_returns.iloc[-1]
cumulative_returns_final.name = "Final Cumulative Returns (%)"
cumulative_returns_final = round(cumulative_returns_final.sort_values(ascending=False) , 1)
print("Final Cumulative Returns in %:")
display(cumulative_returns_final.all)
Final Cumulative Returns in (%) in 666 days of evaluation: 
since March 11, 2024 until January 06, 2026
Final Cumulative Returns in %:
<bound method Series.all of Ticker
PLTR     568.4
SOFI     279.8
TSLA     157.6
GOOGL    135.5
GOOG     134.6
TSM      126.3
NVDA     115.0
GLD      102.7
CAT       86.5
OMAB      79.5
BAC       66.5
NU        62.1
AAPL      57.8
NFLX      51.2
SPYG      51.1
VOOG      51.1
VGT       46.8
VUG       45.3
QQQM      42.5
CRWD      41.4
SOXX      41.3
VOO       37.9
CME       37.0
SPG       32.3
VYM       31.2
META      31.0
VTV       28.8
MAR       26.5
ASML      25.5
QYLD      24.3
LMT       23.9
BRK-B     23.7
VOOV      22.8
KO        21.1
SPYD      19.6
MSFT      17.9
MCD        6.8
DIS        5.5
PBR        3.5
PSA       -3.5
HD        -3.8
QSR      -10.2
UNH      -25.6
ADBE     -39.9
SERV     -46.7
Name: Final Cumulative Returns (%), dtype: float64>

Summary Final Cumulated Returns¶

In [37]:
print(f"Number of days of evaluation (period): {(today - cumulative_returns.index[0]).days}")
print(f"From {cumulative_returns.index[0].date().strftime("%B %d, %Y")} until {today.date().strftime("%B %d, %Y")}")

print(f"The asset with the best cumulated return in the Period '{cumulative_returns_final.index[0]}' with {cumulative_returns_final.iloc[0]:,.5}% in the period")
print(f"The asset with the worst cumulative return in the Period '{cumulative_returns_final.index[-1]}' with {cumulative_returns_final.iloc[-1]:,.5}% in the period")
Number of days of evaluation (period): 666
From March 11, 2024 until January 06, 2026
The asset with the best cumulated return in the Period 'PLTR' with 568.4% in the period
The asset with the worst cumulative return in the Period 'SERV' with -46.7% in the period

Risk, Annualized Volatility¶

TS303_yfinance Indices Bursatiles.ipynb

Asset Volatility (risk or std)

$$ \sigma = risk = \sqrt{\frac {\sum(r - \bar{r})^2}{n-1}} $$

In [38]:
# Yearly volatility (risk)

# StdDev of daily returns in a 252-days year
annualized_volatility = daily_returns.std() * np.sqrt(252)

# Percentage (%)
annualized_volatility_percent = annualized_volatility * 100

# DataFrame of Annualized Volatility
volatility_df = pd.DataFrame(annualized_volatility_percent, columns=["Volatility (%)"])

print("Annualized Assets Volatility:")

volatility_df = round(volatility_df.sort_values(by="Volatility (%)", ascending=False), 2)

display(volatility_df)
Annualized Assets Volatility:
Volatility (%)
Ticker
SERV 209.15
TSLA 64.71
PLTR 63.15
SOFI 57.83
NVDA 50.56
CRWD 45.66
ASML 43.30
NU 42.78
TSM 40.79
UNH 39.95
SOXX 37.78
META 35.01
OMAB 34.40
ADBE 34.03
NFLX 31.53
GOOGL 30.36
GOOG 29.97
CAT 29.92
PBR 29.26
AAPL 28.62
DIS 27.16
MAR 25.89
VGT 25.43
BAC 25.18
SPG 23.62
LMT 22.85
PSA 22.66
QSR 22.63
MSFT 22.50
HD 22.24
QQQM 21.11
VUG 21.04
SPYG 20.98
VOOG 20.90
MCD 18.39
GLD 18.22
CME 17.54
BRK-B 17.04
VOO 16.15
KO 15.68
QYLD 14.87
SPYD 14.34
VYM 13.64
VOOV 13.62
VTV 13.33

Stats of Volatility

In [39]:
display(volatility_df.describe()) # all stocks
Volatility (%)
count 45.000000
mean 32.794889
std 29.833583
min 13.330000
25% 20.900000
50% 25.430000
75% 35.010000
max 209.150000

Summary Risk, Volatility¶

In [40]:
# summary and stats

print(f"Number of days of evaluation: {(today - cumulative_returns.index[0]).days}")
print(f"From {cumulative_returns.index[0].date().strftime("%B %d, %Y")} until {today.date().strftime("%B %d, %Y")}")

print(f"The asset with the biggest Annualized Volatility is '{volatility_df.index[0]}' with a {volatility_df.iloc[0].values[0]:,.5} %")
print(f"The asset with the lowest Annualized Volatility is '{volatility_df.index[-1]}' with a {volatility_df.iloc[-1].values[0]:,.5} %")
print(f"The Average Annualized Volatility of all assets is: {volatility_df.mean().values[0]:,.5} %")
Number of days of evaluation: 666
From March 11, 2024 until January 06, 2026
The asset with the biggest Annualized Volatility is 'SERV' with a 209.15 %
The asset with the lowest Annualized Volatility is 'VTV' with a 13.33 %
The Average Annualized Volatility of all assets is: 32.795 %

Dividend Yields (%)¶

In [153]:
# ANNUAL DIVIDEND YIELDS
yields = functions.plot_annual_dividnd_yields(daily_returns)
Annual Dividend Yields from yf 'dividendYield' (%):
AAPL      0.39
ADBE      0.00
ASML      0.60
BAC       1.97
BRK-B     0.00
CAT       0.98
CME       1.82
CRWD      0.00
DIS       1.31
GLD       0.00
GOOG      0.26
GOOGL     0.27
HD        2.67
KO        3.00
LMT       2.70
MAR       0.86
MCD       2.48
META      0.32
MSFT      0.77
NFLX      0.00
NU        0.00
NVDA      0.02
OMAB      4.48
PBR      14.31
PLTR      0.00
PSA       4.60
QQQM      0.49
QSR       3.72
QYLD     10.46
SERV      0.00
SOFI      0.00
SOXX      0.55
SPG       4.81
SPYD      4.46
SPYG      0.53
TSLA      0.00
TSM       1.04
UNH       2.58
VGT       0.41
VOO       1.12
VOOG      0.48
VOOV      1.80
VTV       2.05
VUG       0.42
VYM       2.42
Name: dividendYield, dtype: float64

Initial Portfolio (Original)¶

Assets Weights¶

Compute Assets weights from the original portfolio

In [42]:
# call the original portfolio [Ticker, Current QTY]
file_df

# Close prices df
close_prices = prices.iloc[-1]
close_prices.name = "Close Price"

# Merge original df with Close Prices
weights_df = pd.merge(file_df, close_prices, on='Ticker', how='inner')

# Add column of Amount Invested for each asset
weights_df['Investment'] = weights_df["Current QTY"] * weights_df["Close Price"]

# Calculate the Total Invested
Total_invested = weights_df['Investment'].sum()
print(f"Total Invested: ${Total_invested:,.2f}")

# Add column of weights of each asset
weights_df['Weights'] = weights_df['Investment'] / Total_invested
print(f"The sum of the weights is: {weights_df['Weights'].sum()}")

weights_df.set_index('Ticker', inplace=True)
weights_df.sort_values(by='Investment', ascending=False, inplace=True)
print("Sorted by Invested amount ($):")
display(round(weights_df, 2))
Total Invested: $68,831.74
The sum of the weights is: 1.0
Sorted by Invested amount ($):
Current QTY Close Price Investment Weights
Ticker
NVDA 100.53 188.12 18911.48 0.27
MSFT 19.75 472.85 9339.75 0.14
AAPL 17.04 267.26 4554.12 0.07
PLTR 21.02 174.04 3658.95 0.05
TSM 10.03 322.25 3233.66 0.05
VOOG 6.30 446.51 2811.39 0.04
GLD 6.45 408.76 2636.24 0.04
TSLA 4.92 451.67 2224.30 0.03
VOOV 10.36 207.51 2150.75 0.03
QYLD 100.86 17.76 1791.28 0.03
UNH 5.04 342.02 1722.64 0.03
QQQM 6.38 254.43 1622.13 0.02
VGT 2.02 757.42 1529.25 0.02
DIS 13.03 114.07 1486.23 0.02
VOO 2.29 632.46 1447.28 0.02
META 1.58 658.79 1039.45 0.02
PBR 87.44 11.74 1026.60 0.01
SOXX 3.05 318.06 968.76 0.01
CRWD 1.46 456.55 664.40 0.01
GOOGL 2.01 316.54 636.79 0.01
CME 2.00 275.06 550.12 0.01
GOOG 1.61 317.32 512.10 0.01
VUG 1.01 488.45 492.50 0.01
KO 6.23 67.94 423.06 0.01
QSR 5.26 66.74 350.73 0.01
ASML 0.26 1228.19 324.45 0.00
MCD 1.05 299.86 315.95 0.00
VYM 2.10 145.82 306.92 0.00
NU 13.62 17.94 244.39 0.00
OMAB 2.16 109.26 236.47 0.00
CAT 0.33 616.10 205.93 0.00
LMT 0.32 511.57 163.25 0.00
SPYD 3.65 43.69 159.31 0.00
SOFI 5.32 29.28 155.76 0.00
ADBE 0.47 331.56 154.20 0.00
MAR 0.46 311.03 142.13 0.00
BRK-B 0.24 498.52 122.03 0.00
NFLX 1.20 91.46 109.50 0.00
SPYG 1.02 107.16 109.19 0.00
BAC 1.47 56.89 83.56 0.00
VTV 0.40 194.65 78.82 0.00
PSA 0.19 260.90 49.62 0.00
SERV 3.36 12.68 42.67 0.00
SPG 0.20 183.11 36.40 0.00
HD 0.02 344.09 7.21 0.00

TO-DO: Grafica de Pastel con los Porcentajes

Grafica por Sectores, Industrias

Portfolio Daily Returns¶

In [43]:
#checking the shapes of the objects to multiply
# Daily Returns Expresed in FRACTION. 
print("Portfolio daily returns = [Daily Returns] x [weights]")
print(f"daily_returns shape: {daily_returns.shape}")
print(f"weights_df shape: {weights_df['Weights'].shape}")
print(f"Matrix multiplication [{daily_returns.shape[0]} x {daily_returns.shape[1]}] x [{weights_df['Weights'].shape[0]} x 1] = [{daily_returns.shape[0]} x 1]")
Portfolio daily returns = [Daily Returns] x [weights]
daily_returns shape: (457, 45)
weights_df shape: (45,)
Matrix multiplication [457 x 45] x [45 x 1] = [457 x 1]
In [44]:
# option 1: Portfolio's Daily Returns: Matrix multiplication (see above)
portfolio_daily_returns = (daily_returns @ weights_df['Weights'])

# option 2: Portfolio's Daily Returns: weighted sum of the daily returns of each asset
# portfolio_daily_returns = (daily_returns * weights_df['Weights']).sum(axis=1)


portfolio_daily_returns.name = 'portfolio daily returns'
print("portfolio_daily_returns:")
display(portfolio_daily_returns) # Expresed in FRACTION (not percentage)
portfolio_daily_returns:
Date
2024-03-11   -0.009023
2024-03-12    0.027501
2024-03-13   -0.006010
2024-03-14   -0.009048
2024-03-15   -0.009670
                ...   
2025-12-29   -0.008831
2025-12-30   -0.002100
2025-12-31   -0.006179
2026-01-02    0.000196
2026-01-05    0.005278
Name: portfolio daily returns, Length: 457, dtype: float64
In [45]:
results_df = functions.plot_returns_distributions(pd.DataFrame(portfolio_daily_returns), num_cols=4)
No description has been provided for this image
================================================================================
FAT-TAIL RISK RANKING (Sorted by Highest Kurtosis)
================================================================================
  Mean Median Std Dev Skewness Kurtosis
Ticker          
portfolio daily returns 0.0015 0.0025 0.0165 0.5720 10.2486
In [46]:
# -- PLOT DAILY RETURNS --
functions.plot_daily_returns(pd.DataFrame(portfolio_daily_returns))

Portfolio Annualized Returns¶

In [47]:
# Annualized Returns
annualized_return_portfolio = portfolio_daily_returns.mean() * 250
print("Annualized Returns (%):")
print(f"{round(annualized_return_portfolio, 2) * 100}%")
Annualized Returns (%):
38.0%

Portfolio Cumulative Returns¶

In [48]:
# Portfolio's Cumulative daily Returns
portfolio_cumulative_returns = (1 + portfolio_daily_returns).cumprod()
portfolio_cumulative_returns = (portfolio_cumulative_returns - 1) * 100
portfolio_cumulative_returns = portfolio_cumulative_returns.rename('portfolio cumulative returns')
print("Portfolio Daily Cumulative Returns:")
display(portfolio_cumulative_returns) 
Portfolio Daily Cumulative Returns:
Date
2024-03-11    -0.902260
2024-03-12     1.823045
2024-03-13     1.211137
2024-03-14     0.295371
2024-03-15    -0.674509
                ...    
2025-12-29    88.416738
2025-12-30    88.021052
2025-12-31    86.859277
2026-01-02    86.895959
2026-01-05    87.882465
Name: portfolio cumulative returns, Length: 457, dtype: float64
In [49]:
# -- PLOT CUMULATIVE RETURNS --
functions.plot_cumulative_returns(pd.DataFrame(portfolio_cumulative_returns))

Portfolio Cumulative Final

In [50]:
# Portfolio's final comulated return
portafolio_cumulative_final = portfolio_cumulative_returns.iloc[-1]
print(f"portafolio_cumulative Returns_final: {portafolio_cumulative_final:,.4}%")
portafolio_cumulative Returns_final: 87.88%

Portfolio Volatility (Annualized)¶

In [51]:
# Compute annualized Volatility of the Portfolio
portfolio_annualized_volatility = portfolio_daily_returns.std() * np.sqrt(252)

# Convert to percentage
portfolio_annualized_volatility_perc = portfolio_annualized_volatility * 100

print(f"Portfolio Annualized Volatility: {portfolio_annualized_volatility_perc:,.4}%")
Portfolio Annualized Volatility: 26.13%

Portfolio Sharpe Ratio¶

$$ Sharpe = \frac{R_p - R_f}{\sigma_p} = \frac{\mu_p - r_f}{\sigma_p}$$

Where:

  • $R_p$: Expected Portfolio Return, $\mu$ (average rate of return)
  • $R_f$: Risk Free Rate (can be 0 if ignored)
  • $\sigma_p$: Portfolio Risk (StdDev) Standard Deviation of the portfolio's excess return.
In [52]:
portfolio_annualized_return = portfolio_daily_returns.mean()*252 #Annualized
portfolio_sr = round((portfolio_annualized_return - (risk_free))/portfolio_annualized_volatility, 2)
print(f"Portfolio Sharpe Ratio: {portfolio_sr}")
Portfolio Sharpe Ratio: 1.3

Summary¶

In [53]:
print(f"Number of days of evaluation: {(today - cumulative_returns.index[0]).days} days")
print(f"From {cumulative_returns.index[0].date().strftime("%B %d, %Y")} until {today.date().strftime("%B %d, %Y")}")

print(f"\nTotal Invested: ${Total_invested:,.2f}")
print(f"Portafolio_cumulative returns_final (all period): {portafolio_cumulative_final:,.4}%")
print(f"Portfolio Annualized Average Returns: {portfolio_annualized_return:,.4%}")
print(f"Portfolio Annualized Volatility: {portfolio_annualized_volatility_perc:,.4}%")
print(f"Portfolio Sharpe Ratio: {portfolio_sr}")
Number of days of evaluation: 666 days
From March 11, 2024 until January 06, 2026

Total Invested: $68,831.74
Portafolio_cumulative returns_final (all period): 87.88%
Portfolio Annualized Average Returns: 38.1843%
Portfolio Annualized Volatility: 26.13%
Portfolio Sharpe Ratio: 1.3

Benchmarks (Indices)¶

Daily Index Levels¶

In [54]:
# Benchmark Indices
benchmark_indices = {
    "EE.UU. (S&P 500)": "^GSPC",
    "EE.UU. (NASDAQ)": "^IXIC",
    "EE.UU. (DJIA)": "^DJI",
    "EE.UU. (Russell 100)": "^RUI",
    "México (IPC)": "^MXX",
    "Japón (Nikkei 225)": "^N225",
    "Alemania (DAX)": "^GDAXI",
    "Reino Unido (FTSE 100)": "^FTSE"
}

# get data from yahoo finance
benchmarks_prices = yf.download(list(benchmark_indices.values()), start=first_valid_dates.iloc[0], end=today, auto_adjust=True)["Close"]

# Rename columns
benchmarks_prices.columns = list(benchmark_indices.keys())

print("Benchmark Indices levels:")
display(benchmarks_prices.tail(5))
[*********************100%***********************]  8 of 8 completed
Benchmark Indices levels:

EE.UU. (S&P 500) EE.UU. (NASDAQ) EE.UU. (DJIA) EE.UU. (Russell 100) México (IPC) Japón (Nikkei 225) Alemania (DAX) Reino Unido (FTSE 100)
Date
2025-12-29 48461.929688 9866.500000 24351.119141 6905.740234 23474.349609 65347.078125 50526.921875 3766.949951
2025-12-30 48367.058594 9940.700195 24490.410156 6896.240234 23419.080078 64366.699219 50339.480469 3761.540039
2025-12-31 48063.289062 9931.400391 NaN 6845.500000 23241.990234 64308.289062 NaN 3732.870117
2026-01-02 48382.390625 9951.099609 24539.339844 6858.470215 23235.630859 64141.359375 NaN 3742.669922
2026-01-05 48977.179688 10004.599609 24868.689453 6902.049805 23395.820312 65014.371094 51832.800781 3769.020020
In [55]:
# -- PLOT ASSET PRICES --
functions.plot_prices(prices=benchmarks_prices, yaxis_label="Index Level")

Normalized Index Levels¶

In [56]:
# Normalized Index Levels
benchmarks_prices_normalized = (benchmarks_prices / benchmarks_prices.iloc[0]) * 100

# -- PLOT ASSET PRICES --
functions.plot_prices(prices=benchmarks_prices_normalized, yaxis_label="Index Level")

Daily Returns¶

In [57]:
# Daily Returns
daily_returns_bm = benchmarks_prices.pct_change(fill_method=None)

# limpieza básica de daily_returns (elimina la primera fila con NaN)
daily_returns_bm = daily_returns_bm.dropna()
daily_returns_bm.tail(5)
Out[57]:
EE.UU. (S&P 500) EE.UU. (NASDAQ) EE.UU. (DJIA) EE.UU. (Russell 100) México (IPC) Japón (Nikkei 225) Alemania (DAX) Reino Unido (FTSE 100)
Date
2025-12-18 0.001376 0.006497 0.009971 0.007934 0.013794 0.020459 -0.010316 0.007753
2025-12-19 0.003817 0.006058 0.003674 0.008818 0.013095 0.002502 0.010320 0.008832
2025-12-22 0.004732 -0.003173 -0.000182 0.006436 0.005200 0.012682 0.018082 0.006793
2025-12-23 0.001649 0.002352 0.002310 0.004550 0.005677 0.012616 0.000208 0.003815
2025-12-30 -0.001958 0.007520 0.005720 -0.001376 -0.002354 -0.015003 -0.003710 -0.001436
In [58]:
results_df = functions.plot_returns_distributions(daily_returns_bm, num_cols=4)
No description has been provided for this image
================================================================================
FAT-TAIL RISK RANKING (Sorted by Highest Kurtosis)
================================================================================
  Mean Median Std Dev Skewness Kurtosis
Ticker          
EE.UU. (Russell 100) 0.0007 0.0011 0.0108 0.8268 19.6195
Reino Unido (FTSE 100) 0.0007 0.0012 0.0110 0.8020 19.3017
EE.UU. (S&P 500) 0.0004 0.0007 0.0096 0.7444 15.7375
México (IPC) 0.0011 0.0022 0.0144 0.8647 15.0550
Alemania (DAX) 0.0006 0.0007 0.0172 -0.4420 14.0991
EE.UU. (NASDAQ) 0.0005 0.0008 0.0074 -1.3876 10.4678
Japón (Nikkei 225) 0.0003 0.0003 0.0101 -0.6549 5.5900
EE.UU. (DJIA) 0.0007 0.0006 0.0104 -0.3767 3.3813
In [59]:
functions.plot_daily_returns(daily_returns_bm)

Annualized Returns¶

In [60]:
# Annualized Returns
annualized_return_bm = daily_returns_bm.mean() * 252 
annualized_return_bm.name = "Annualized Returns (%):"
annualized_return_bm = annualized_return_bm.sort_values(ascending=False)
print("Annualized Returns (%):")
print(round(annualized_return_bm, 2)*100)
Annualized Returns (%):
México (IPC)              27.0
EE.UU. (DJIA)             17.0
EE.UU. (Russell 100)      17.0
Reino Unido (FTSE 100)    16.0
Alemania (DAX)            15.0
EE.UU. (NASDAQ)           13.0
EE.UU. (S&P 500)          11.0
Japón (Nikkei 225)         8.0
Name: Annualized Returns (%):, dtype: float64

Cumulative Daily Returns¶

In [61]:
cumulative_returns_bm = (1 + daily_returns_bm).cumprod()
cumulative_returns_bm = (cumulative_returns_bm - 1) * 100
display(cumulative_returns_bm.tail(5))
EE.UU. (S&P 500) EE.UU. (NASDAQ) EE.UU. (DJIA) EE.UU. (Russell 100) México (IPC) Japón (Nikkei 225) Alemania (DAX) Reino Unido (FTSE 100)
Date
2025-12-18 13.266130 17.128699 23.859387 22.352113 37.085787 8.299930 14.406983 21.228400
2025-12-19 13.698483 17.838302 24.314403 23.431021 38.880875 8.570921 15.587699 22.299108
2025-12-22 14.236539 17.464450 24.291731 24.225485 39.603123 9.947766 17.677733 23.129847
2025-12-23 14.424869 17.740671 24.578813 24.790759 40.395675 11.334842 17.702203 23.599630
2025-12-30 14.200866 18.626130 25.291417 24.619089 40.065119 9.664525 17.265559 23.422122
In [62]:
functions.plot_cumulative_returns(cumulative_returns_bm)
In [63]:
# Final Cumulative Return in the period of evaluation for all Indices
print("Final Cumulative Returns (%)")
# cumulative_returns_final_bm = (1 + daily_returns_bm).prod() -1
cumulative_returns_final_bm = cumulative_returns_bm.iloc[-1]
cumulative_returns_final_bm = cumulative_returns_final_bm.sort_values(ascending=False)
cumulative_returns_final_bm = cumulative_returns_final_bm.rename("final cumulative returns Benchmarks (%)")
display(cumulative_returns_final_bm)
Final Cumulative Returns (%)
México (IPC)              40.065119
EE.UU. (DJIA)             25.291417
EE.UU. (Russell 100)      24.619089
Reino Unido (FTSE 100)    23.422122
EE.UU. (NASDAQ)           18.626130
Alemania (DAX)            17.265559
EE.UU. (S&P 500)          14.200866
Japón (Nikkei 225)         9.664525
Name: final cumulative returns Benchmarks (%), dtype: float64

Volatility¶

In [64]:
#Annualized Volatility
annualized_volatility_bm = daily_returns_bm.std() * np.sqrt(252)

#Annualized Volatility Percentage
annualized_volatility_bm_perc = annualized_volatility_bm * 100

annualized_volatility_bm_perc.rename('Volatility Benchmarks (%)', inplace=True)

print("Volatility of Benchmark Indices (%):")
annualized_volatility_bm_perc.sort_values(ascending=False, inplace=True)
display(annualized_volatility_bm_perc)
Volatility of Benchmark Indices (%):
Alemania (DAX)            27.297079
México (IPC)              22.904904
Reino Unido (FTSE 100)    17.402071
EE.UU. (Russell 100)      17.223190
EE.UU. (DJIA)             16.435356
Japón (Nikkei 225)        15.980321
EE.UU. (S&P 500)          15.286125
EE.UU. (NASDAQ)           11.685753
Name: Volatility Benchmarks (%), dtype: float64

Sharpe Ratios¶

$$ Sharpe = \frac{R_p - R_f}{\sigma_p} = \frac{\mu_p - r_f}{\sigma_p}$$

Where:

  • $R_p$: Expected Portfolio Return, $\mu$
  • $R_f$: Risk Free Rate (can be 0 if ignored)
  • $\sigma_p$: Portfolio Risk (StdDev)
In [65]:
# Sharpe Ratios
benchmark_sr = round((annualized_return_bm - (risk_free))/annualized_volatility_bm, 2)
benchmark_sr.rename('Sharpe Ratio', inplace=True)
benchmark_sr.sort_values(ascending=False, inplace=True)
display(benchmark_sr)
México (IPC)              0.98
EE.UU. (DJIA)             0.81
EE.UU. (Russell 100)      0.75
EE.UU. (NASDAQ)           0.74
Reino Unido (FTSE 100)    0.71
EE.UU. (S&P 500)          0.42
Alemania (DAX)            0.40
Japón (Nikkei 225)        0.23
Name: Sharpe Ratio, dtype: float64

Summary¶

In [66]:
# Days of evaluation
no_days_compare = today.date() - first_valid_dates.iloc[0].date()
print(f"\nNumber of days of evaluation: {no_days_compare.days} days.")
print(f"From: {first_valid_dates.iloc[0].date().strftime("%B %d, %Y")} to: {today.date().strftime("%B %d, %Y")}")

# Summary Return and Volatility
print("\nSummary: Benchmark Indices:")
print(f"'{cumulative_returns_final_bm.index[0]}' has the largest total return {cumulative_returns_final_bm.iloc[0]:,.5}%")
print(f"and'{cumulative_returns_final_bm.index[-1]}' the lowest total return {cumulative_returns_final_bm.iloc[-1]:,.3}%")
print(f"\n'{annualized_volatility_bm_perc.index[0]}' has the largest volatility {annualized_volatility_bm_perc.iloc[0]:,.5}%")
print(f"and '{annualized_volatility_bm_perc.index[-1]}' the lowest volatility {annualized_volatility_bm_perc.iloc[-1]:,.5}%")
Number of days of evaluation: 669 days.
From: March 08, 2024 to: January 06, 2026

Summary: Benchmark Indices:
'México (IPC)' has the largest total return 40.065%
and'Japón (Nikkei 225)' the lowest total return 9.66%

'Alemania (DAX)' has the largest volatility 27.297%
and 'EE.UU. (NASDAQ)' the lowest volatility 11.686%

Compare Initial Portfolio and Indices¶

Daily Returns¶

Combine dataframes

In [67]:
# Combine Daily Returns of Initial Porfolio and 
# Benchmark Indices in a single dataFrame to plot

# convert portfolio_daily_returns to datafre to merge it with Indices daily returns
portfolio_daily_returns_df = pd.DataFrame(portfolio_daily_returns)
portfolio_daily_returns_df.rename(columns={'portfolio daily returns':'Initial Portfolio'}, inplace=True)

# check if the lenghts of the dataframes match
if len(portfolio_daily_returns_df) != len(daily_returns_bm):
    print(f"Lengths of DataFrames don't match {len(portfolio_daily_returns_df)} vs {len(daily_returns_bm)} but a left merge will help")
    print("There are more dates in one dataframe than the other")


# Merge Daily Returns of Benchmark Indices and Initial Portfolio
# Note: Left-Merge on Benchmark daily returns because they don't operate on weekends or bank holidays
# thus we compare both benchmark and portfolio using the same labor day dates only.
merge_daily_returns = pd.merge(daily_returns_bm, portfolio_daily_returns_df, on='Date', how="left")
print("\nDaily Returns (merged portfolio and indices):")
display(merge_daily_returns.tail(5))
Lengths of DataFrames don't match 457 vs 354 but a left merge will help
There are more dates in one dataframe than the other

Daily Returns (merged portfolio and indices):
EE.UU. (S&P 500) EE.UU. (NASDAQ) EE.UU. (DJIA) EE.UU. (Russell 100) México (IPC) Japón (Nikkei 225) Alemania (DAX) Reino Unido (FTSE 100) Initial Portfolio
Date
2025-12-18 0.001376 0.006497 0.009971 0.007934 0.013794 0.020459 -0.010316 0.007753 0.014941
2025-12-19 0.003817 0.006058 0.003674 0.008818 0.013095 0.002502 0.010320 0.008832 0.017300
2025-12-22 0.004732 -0.003173 -0.000182 0.006436 0.005200 0.012682 0.018082 0.006793 0.007409
2025-12-23 0.001649 0.002352 0.002310 0.004550 0.005677 0.012616 0.000208 0.003815 0.011454
2025-12-30 -0.001958 0.007520 0.005720 -0.001376 -0.002354 -0.015003 -0.003710 -0.001436 -0.002100
In [68]:
results_df = functions.plot_returns_distributions(merge_daily_returns, num_cols=4)
No description has been provided for this image
================================================================================
FAT-TAIL RISK RANKING (Sorted by Highest Kurtosis)
================================================================================
  Mean Median Std Dev Skewness Kurtosis
Ticker          
EE.UU. (Russell 100) 0.0007 0.0011 0.0108 0.8268 19.6195
Reino Unido (FTSE 100) 0.0007 0.0012 0.0110 0.8020 19.3017
EE.UU. (S&P 500) 0.0004 0.0007 0.0096 0.7444 15.7375
México (IPC) 0.0011 0.0022 0.0144 0.8647 15.0550
Alemania (DAX) 0.0006 0.0007 0.0172 -0.4420 14.0991
Initial Portfolio 0.0019 0.0037 0.0172 0.6294 10.9811
EE.UU. (NASDAQ) 0.0005 0.0008 0.0074 -1.3876 10.4678
Japón (Nikkei 225) 0.0003 0.0003 0.0101 -0.6549 5.5900
EE.UU. (DJIA) 0.0007 0.0006 0.0104 -0.3767 3.3813
In [69]:
functions.plot_daily_returns(merge_daily_returns)

Cumulative Returns¶

In [70]:
# Combine Comulative Returns of Initial Porfolio and Benchmark Indices in a single dataFrame to plot

# convert portfolio_cumulative_returns to datafre to merge it with Indices returns
portfolio_cumulative_returns_df = pd.DataFrame(portfolio_cumulative_returns)
portfolio_cumulative_returns_df.rename(columns={'portfolio cumulative returns':'Initial Portfolio'}, inplace=True)

# check if the lenghts of the dataframes match
if len(portfolio_cumulative_returns_df) != len(cumulative_returns_bm):
    print(f"Lengths of DataFrames don't match {len(portfolio_cumulative_returns_df)} vs {len(cumulative_returns_bm)} but a left merge will help")


# Merge Cumulative Returns of Benchmark Indices and Initial Portfolio
# Note: Left-Merge on Benchmark cumulative returns because they don't operate on weekends or bank holidays
# thus we compare both benchmark and portfolio using the same labor day dates only.
merge_cumulative_returns = pd.merge(cumulative_returns_bm, portfolio_cumulative_returns_df, on='Date', how="left")
print("\nDaily Cumulative Returns (merged portfolio and indices):")
display(merge_cumulative_returns.tail(5))
Lengths of DataFrames don't match 457 vs 354 but a left merge will help

Daily Cumulative Returns (merged portfolio and indices):
EE.UU. (S&P 500) EE.UU. (NASDAQ) EE.UU. (DJIA) EE.UU. (Russell 100) México (IPC) Japón (Nikkei 225) Alemania (DAX) Reino Unido (FTSE 100) Initial Portfolio
Date
2025-12-18 13.266130 17.128699 23.859387 22.352113 37.085787 8.299930 14.406983 21.228400 82.948254
2025-12-19 13.698483 17.838302 24.314403 23.431021 38.880875 8.570921 15.587699 22.299108 86.113241
2025-12-22 14.236539 17.464450 24.291731 24.225485 39.603123 9.947766 17.677733 23.129847 87.492159
2025-12-23 14.424869 17.740671 24.578813 24.790759 40.395675 11.334842 17.702203 23.599630 89.639700
2025-12-30 14.200866 18.626130 25.291417 24.619089 40.065119 9.664525 17.265559 23.422122 88.021052
In [71]:
functions.plot_cumulative_returns(merge_cumulative_returns)
In [72]:
# Total Cumulative Returns
print("\nTotal (Final) cumulative Returns (%):")
merge_cumulative_returns_finals = merge_cumulative_returns.iloc[-1].copy()
merge_cumulative_returns_finals.rename('Final Cumulative Returns (%)', inplace=True)
merge_cumulative_returns_finals.sort_values(ascending=False, inplace=True)
display(merge_cumulative_returns_finals)
Total (Final) cumulative Returns (%):
Initial Portfolio         88.021052
México (IPC)              40.065119
EE.UU. (DJIA)             25.291417
EE.UU. (Russell 100)      24.619089
Reino Unido (FTSE 100)    23.422122
EE.UU. (NASDAQ)           18.626130
Alemania (DAX)            17.265559
EE.UU. (S&P 500)          14.200866
Japón (Nikkei 225)         9.664525
Name: Final Cumulative Returns (%), dtype: float64

Volatility¶

In [73]:
# Anualized Volatility of portfolio + Benchmarks
print('\nAnnualized Volatility (%):')
merge_annualized_volatility = merge_daily_returns.std() * np.sqrt(252)
merge_annualized_volatility = merge_annualized_volatility * 100
merge_annualized_volatility.rename('Annualized Volatility (%)', inplace=True)
merge_annualized_volatility.sort_values(ascending=False, inplace=True)
display(merge_annualized_volatility)
Annualized Volatility (%):
Alemania (DAX)            27.297079
Initial Portfolio         27.296752
México (IPC)              22.904904
Reino Unido (FTSE 100)    17.402071
EE.UU. (Russell 100)      17.223190
EE.UU. (DJIA)             16.435356
Japón (Nikkei 225)        15.980321
EE.UU. (S&P 500)          15.286125
EE.UU. (NASDAQ)           11.685753
Name: Annualized Volatility (%), dtype: float64

Sharpe Ratio¶

In [74]:
sharpe_ratio = pd.DataFrame()
sharpe_ratio['Annualized Returns (%)'] = merge_daily_returns.mean()*252*100 #Annualized returns
sharpe_ratio = pd.concat([sharpe_ratio['Annualized Returns (%)'], merge_annualized_volatility], axis=1)
sharpe_ratio['Sharpe Ratio'] = (sharpe_ratio['Annualized Returns (%)'] - (risk_free*100)) / (sharpe_ratio['Annualized Volatility (%)'])
sharpe_ratio.sort_values(by='Sharpe Ratio', ascending=False, inplace=True)
sharpe_ratio
Out[74]:
Annualized Returns (%) Annualized Volatility (%) Sharpe Ratio
Initial Portfolio 48.292658 27.296752 1.617066
México (IPC) 26.592593 22.904904 0.979729
EE.UU. (DJIA) 17.405433 16.435356 0.806398
EE.UU. (Russell 100) 17.143433 17.223190 0.754299
EE.UU. (NASDAQ) 12.847404 11.685753 0.744103
Reino Unido (FTSE 100) 16.486908 17.402071 0.708818
EE.UU. (S&P 500) 10.614472 15.286125 0.422767
Alemania (DAX) 15.081337 27.297079 0.400385
Japón (Nikkei 225) 7.847153 15.980321 0.231231

TO-do: Poner nota explicatoria de por que el SR es 1.58 aqui contra 1.3 arriba

In [75]:
#fig, ax = plt.subplots()

ax = sharpe_ratio.plot.scatter(
                        x=sharpe_ratio.columns[1], 
                        y=sharpe_ratio.columns[0],
                        c=sharpe_ratio.columns[2], 
                        colormap='coolwarm', 
                        alpha=1.0,
                        figsize=(10,8))
for i, label in enumerate(round(sharpe_ratio['Sharpe Ratio'],3)):
    ax.text(sharpe_ratio['Annualized Volatility (%)'].iloc[i] + 0.05, sharpe_ratio['Annualized Returns (%)'].iloc[i], label)
    ax.text(sharpe_ratio['Annualized Volatility (%)'].iloc[i] + 0.05, sharpe_ratio['Annualized Returns (%)'].iloc[i] + 1, sharpe_ratio.index[i])

ax.figure.axes[1].set_ylabel('Sharpe Ratio level')

plt.title('Sharpe Ratio')
plt.xlabel('Risk (%)')
plt.ylabel('Return (%)')
plt.grid()
plt.show()
No description has been provided for this image

Summary¶

In [76]:
# Days of evaluation
no_days_compare = today.date() - first_valid_dates.iloc[0].date()
print(f"\nNumber of days of evaluation: {no_days_compare.days} days. From: {first_valid_dates.iloc[0].date().strftime("%B %d, %Y")} to: {today.date().strftime("%B %d, %Y")}")

# Summary Return, Volatility and Sharp Ratio
print("\nSummary: Benchmark Indices + Initial Portfolio")
print("\nReturns:")
print(f"'{merge_cumulative_returns_finals.index[0]}' has the largest total return {merge_cumulative_returns_finals.iloc[0]:,.5}%")
print(f"and'{merge_cumulative_returns_finals.index[-1]}' the lowest total return {merge_cumulative_returns_finals.iloc[-1]:,.3}%")
print("\nVolatility:")
print(f"'{merge_annualized_volatility.index[0]}' has the largest volatility {merge_annualized_volatility.iloc[0]:,.5}%")
print(f"and '{merge_annualized_volatility.index[-1]}' the lowest volatility {merge_annualized_volatility.iloc[-1]:,.5}%")
print("\nSharp Ratio:")
print(f"'{sharpe_ratio.index[0]}' has the best Sharpe Ratio {sharpe_ratio['Sharpe Ratio'].iloc[0]:,.4}")
print(f"and '{sharpe_ratio.index[-1]}' the worst Sharp Ratio {sharpe_ratio['Sharpe Ratio'].iloc[-1]:,.3}")
Number of days of evaluation: 669 days. From: March 08, 2024 to: January 06, 2026

Summary: Benchmark Indices + Initial Portfolio

Returns:
'Initial Portfolio' has the largest total return 88.021%
and'Japón (Nikkei 225)' the lowest total return 9.66%

Volatility:
'Alemania (DAX)' has the largest volatility 27.297%
and 'EE.UU. (NASDAQ)' the lowest volatility 11.686%

Sharp Ratio:
'Initial Portfolio' has the best Sharpe Ratio 1.617
and 'Japón (Nikkei 225)' the worst Sharp Ratio 0.231

Buy and Hold¶

B&H - Stocks (Initial Portfolio)¶

Initial Investment $100,000 in each Asset

In [77]:
# B&H Stocks in initial Portfolio
functions.buy_and_hold_strategy(cumulative_returns, 100000, "Portfolio Stocks")
--- Investment Analysis: Portfolio Stocks ---
From: March 11, 2024 to January 05, 2026
Period: 665 days
  Initial_Value_USD Final_Value_USD Margin_USD Yield_Perc
Ticker        
PLTR 100,000.00 668,356.33 568,356.33 568.36
SOFI 100,000.00 379,766.54 279,766.54 279.77
TSLA 100,000.00 257,596.68 157,596.68 157.60
GOOGL 100,000.00 235,530.37 135,530.37 135.53
GOOG 100,000.00 234,573.32 134,573.32 134.57
TSM 100,000.00 226,286.44 126,286.44 126.29
NVDA 100,000.00 215,035.17 115,035.17 115.04
GLD 100,000.00 202,727.77 102,727.77 102.73
CAT 100,000.00 186,474.88 86,474.88 86.47
OMAB 100,000.00 179,496.09 79,496.09 79.50
BAC 100,000.00 166,492.49 66,492.49 66.49
NU 100,000.00 162,059.63 62,059.63 62.06
AAPL 100,000.00 157,816.54 57,816.54 57.82
NFLX 100,000.00 151,218.55 51,218.55 51.22
SPYG 100,000.00 151,135.28 51,135.28 51.14
VOOG 100,000.00 151,061.84 51,061.84 51.06
VGT 100,000.00 146,845.11 46,845.11 46.85
VUG 100,000.00 145,287.56 45,287.56 45.29
QQQM 100,000.00 142,482.23 42,482.23 42.48
CRWD 100,000.00 141,412.41 41,412.41 41.41
SOXX 100,000.00 141,291.84 41,291.84 41.29
VOO 100,000.00 137,898.04 37,898.04 37.90
CME 100,000.00 137,022.77 37,022.77 37.02
SPG 100,000.00 132,267.28 32,267.28 32.27
VYM 100,000.00 131,208.38 31,208.38 31.21
META 100,000.00 130,979.08 30,979.08 30.98
VTV 100,000.00 128,760.30 28,760.30 28.76
MAR 100,000.00 126,464.20 26,464.20 26.46
ASML 100,000.00 125,481.10 25,481.10 25.48
QYLD 100,000.00 124,332.93 24,332.93 24.33
LMT 100,000.00 123,931.91 23,931.91 23.93
BRK-B 100,000.00 123,656.21 23,656.21 23.66
VOOV 100,000.00 122,792.89 22,792.89 22.79
KO 100,000.00 121,103.27 21,103.27 21.10
SPYD 100,000.00 119,553.37 19,553.37 19.55
MSFT 100,000.00 117,919.92 17,919.92 17.92
MCD 100,000.00 106,823.52 6,823.52 6.82
DIS 100,000.00 105,490.38 5,490.38 5.49
PBR 100,000.00 103,522.28 3,522.28 3.52
PSA 100,000.00 96,453.83 -3,546.17 -3.55
HD 100,000.00 96,198.91 -3,801.09 -3.80
QSR 100,000.00 89,795.70 -10,204.30 -10.20
UNH 100,000.00 74,392.43 -25,607.57 -25.61
ADBE 100,000.00 60,098.97 -39,901.03 -39.90
SERV 100,000.00 53,277.31 -46,722.69 -46.72

B&H - Indices and Portfolio¶

Initial Investment $100,000 in each Index and Initial Portfolio

In [78]:
# B&H Indices & Portfolio (merged)
functions.buy_and_hold_strategy(merge_cumulative_returns, 100000, "Market Benchmarks Indices and Initital Portfolio")
--- Investment Analysis: Market Benchmarks Indices and Initital Portfolio ---
From: March 11, 2024 to December 30, 2025
Period: 659 days
  Initial_Value_USD Final_Value_USD Margin_USD Yield_Perc
Ticker        
Initial Portfolio 100,000.00 188,021.05 88,021.05 88.02
México (IPC) 100,000.00 140,065.12 40,065.12 40.07
EE.UU. (DJIA) 100,000.00 125,291.42 25,291.42 25.29
EE.UU. (Russell 100) 100,000.00 124,619.09 24,619.09 24.62
Reino Unido (FTSE 100) 100,000.00 123,422.12 23,422.12 23.42
EE.UU. (NASDAQ) 100,000.00 118,626.13 18,626.13 18.63
Alemania (DAX) 100,000.00 117,265.56 17,265.56 17.27
EE.UU. (S&P 500) 100,000.00 114,200.87 14,200.87 14.20
Japón (Nikkei 225) 100,000.00 109,664.53 9,664.53 9.66

Dollar Cost Averaging (DCA)¶

DCA - Stocks (Initial Portfolio)¶

Monthly Investment $100 in each Asset

In [79]:
# For individual stocks
functions.dollar_cost_averaging_strategy(prices, monthly_investment=100, title_suffix="Individual Stocks")
--- DCA Investment Analysis: Individual Stocks ---
  Total_Invested Final_Value_USD Margin_USD Yield_Perc_%
Ticker        
PLTR 2,300.00 7,347.91 5,047.91 219.47
SOFI 2,300.00 5,477.07 3,177.07 138.13
SERV 2,300.00 4,211.12 1,911.12 83.09
GOOGL 2,300.00 3,851.00 1,551.00 67.43
TSM 2,300.00 3,837.66 1,537.66 66.85
GOOG 2,300.00 3,831.20 1,531.20 66.57
CAT 2,300.00 3,662.59 1,362.59 59.24
TSLA 2,300.00 3,647.39 1,347.39 58.58
GLD 2,300.00 3,449.93 1,149.93 50.00
ASML 2,300.00 3,437.25 1,137.25 49.45
NVDA 2,300.00 3,268.31 968.31 42.10
OMAB 2,300.00 3,194.73 894.73 38.90
SOXX 2,300.00 3,152.16 852.16 37.05
NU 2,300.00 3,143.96 843.96 36.69
BAC 2,300.00 2,997.71 697.71 30.34
VGT 2,300.00 2,833.99 533.99 23.22
CRWD 2,300.00 2,833.33 533.33 23.19
VOOG 2,300.00 2,820.17 520.17 22.62
SPYG 2,300.00 2,820.01 520.01 22.61
QQQM 2,300.00 2,775.69 475.69 20.68
VUG 2,300.00 2,769.29 469.29 20.40
MAR 2,300.00 2,760.62 460.62 20.03
AAPL 2,300.00 2,754.08 454.08 19.74
VOO 2,300.00 2,703.26 403.26 17.53
CME 2,300.00 2,692.53 392.53 17.07
VYM 2,300.00 2,632.25 332.25 14.45
SPG 2,300.00 2,629.02 329.02 14.31
VTV 2,300.00 2,616.17 316.17 13.75
QYLD 2,300.00 2,601.34 301.34 13.10
META 2,300.00 2,578.94 278.94 12.13
VOOV 2,300.00 2,568.62 268.62 11.68
LMT 2,300.00 2,491.15 191.15 8.31
MSFT 2,300.00 2,480.81 180.81 7.86
DIS 2,300.00 2,471.46 171.46 7.45
BRK-B 2,300.00 2,434.09 134.09 5.83
NFLX 2,300.00 2,431.42 131.42 5.71
SPYD 2,300.00 2,426.08 126.08 5.48
MCD 2,300.00 2,402.63 102.63 4.46
KO 2,300.00 2,378.96 78.96 3.43
QSR 2,300.00 2,325.70 25.70 1.12
PBR 2,300.00 2,209.65 -90.35 -3.93
HD 2,300.00 2,166.31 -133.69 -5.81
PSA 2,300.00 2,114.69 -185.31 -8.06
UNH 2,300.00 1,932.03 -367.97 -16.00
ADBE 2,300.00 1,828.83 -471.17 -20.49

DCA - Indices¶

Monthly Investment $100 in each Asset

In [80]:
# For Benchmarks and your Portfolio Index
functions.dollar_cost_averaging_strategy(benchmarks_prices, monthly_investment=100, title_suffix="Indices & Portfolio")
--- DCA Investment Analysis: Indices & Portfolio ---
  Total_Invested Final_Value_USD Margin_USD Yield_Perc_%
Ticker        
Alemania (DAX) 2,300.00 2,922.82 622.82 27.08
México (IPC) 2,300.00 2,802.21 502.21 21.84
EE.UU. (DJIA) 2,300.00 2,696.83 396.83 17.25
Japón (Nikkei 225) 2,300.00 2,679.46 379.46 16.50
EE.UU. (Russell 100) 2,300.00 2,671.80 371.80 16.17
Reino Unido (FTSE 100) 2,300.00 2,667.82 367.82 15.99
EE.UU. (NASDAQ) 2,300.00 2,642.57 342.57 14.89
EE.UU. (S&P 500) 2,300.00 2,616.49 316.49 13.76

Momentum¶

Applied to all Stocks in Initial Portfolio

In [81]:
# call the DataFrame of the Final Cumulative Returns, which is the total return of each asset in the period.
cumulative_returns_final

# MOMENTUM: Order from largest to lowest return
momentum_ranking = cumulative_returns_final.sort_values(ascending=False)
print("Momentum Ranking (Annual yield):")
display(momentum_ranking)

# Inverse Ranking
inverse_ranking = cumulative_returns_final.sort_values(ascending=True)
print("Inverse Momentum Ranking (Annual yield):")
display(inverse_ranking)


# Plot Momentum and Inverse momentum

plt.Figure(figsize=(12, 9))
momentum_ranking.plot(kind='bar', title='Momentum Ranking')
plt.ylabel('Yield (%)')
plt.grid(True)
plt.tight_layout()
plt.show()

plt.Figure(figsize=(12, 9))
inverse_ranking.plot(kind='bar', title='Inverse Ranking', color='red')
plt.ylabel('Yield (%)')
plt.grid(True)
plt.tight_layout()
plt.show()
Momentum Ranking (Annual yield):
Ticker
PLTR     568.4
SOFI     279.8
TSLA     157.6
GOOGL    135.5
GOOG     134.6
TSM      126.3
NVDA     115.0
GLD      102.7
CAT       86.5
OMAB      79.5
BAC       66.5
NU        62.1
AAPL      57.8
NFLX      51.2
SPYG      51.1
VOOG      51.1
VGT       46.8
VUG       45.3
QQQM      42.5
CRWD      41.4
SOXX      41.3
VOO       37.9
CME       37.0
SPG       32.3
VYM       31.2
META      31.0
VTV       28.8
MAR       26.5
ASML      25.5
QYLD      24.3
LMT       23.9
BRK-B     23.7
VOOV      22.8
KO        21.1
SPYD      19.6
MSFT      17.9
MCD        6.8
DIS        5.5
PBR        3.5
PSA       -3.5
HD        -3.8
QSR      -10.2
UNH      -25.6
ADBE     -39.9
SERV     -46.7
Name: Final Cumulative Returns (%), dtype: float64
Inverse Momentum Ranking (Annual yield):
Ticker
SERV     -46.7
ADBE     -39.9
UNH      -25.6
QSR      -10.2
HD        -3.8
PSA       -3.5
PBR        3.5
DIS        5.5
MCD        6.8
MSFT      17.9
SPYD      19.6
KO        21.1
VOOV      22.8
BRK-B     23.7
LMT       23.9
QYLD      24.3
ASML      25.5
MAR       26.5
VTV       28.8
META      31.0
VYM       31.2
SPG       32.3
CME       37.0
VOO       37.9
SOXX      41.3
CRWD      41.4
QQQM      42.5
VUG       45.3
VGT       46.8
SPYG      51.1
VOOG      51.1
NFLX      51.2
AAPL      57.8
NU        62.1
BAC       66.5
OMAB      79.5
CAT       86.5
GLD      102.7
NVDA     115.0
TSM      126.3
GOOG     134.6
GOOGL    135.5
TSLA     157.6
SOFI     279.8
PLTR     568.4
Name: Final Cumulative Returns (%), dtype: float64
No description has been provided for this image
No description has been provided for this image

Applied to Indices

In [82]:
# call the DataFrame of the Final Cumulative Returns, which is the total return of each Index in the period.
cumulative_returns_final_bm

# MOMENTUM: Order from largest to lowest return
momentum_ranking_bm = cumulative_returns_final_bm.sort_values(ascending=False)
print("Momentum Ranking (Annual yield) of Benchmark Indices:")
display(momentum_ranking_bm)

# Inverse Ranking
inverse_ranking_bm = cumulative_returns_final_bm.sort_values(ascending=True)
print("Inverse Momentum Ranking (Annual yield) of Benchmark Indices:")
display(inverse_ranking_bm)

# Plot Momentum and Inverse momentum
plt.Figure(figsize=(12, 9))
momentum_ranking_bm.plot(kind='bar', title='Momentum Ranking')
plt.ylabel('Yield (%)')
plt.grid(True)
plt.tight_layout()
plt.show()

plt.Figure(figsize=(12, 9))
inverse_ranking_bm.plot(kind='bar', title='Inverse Ranking', color='red')
plt.ylabel('Yield (%)')
plt.grid(True)
plt.tight_layout()
plt.show()
Momentum Ranking (Annual yield) of Benchmark Indices:
México (IPC)              40.065119
EE.UU. (DJIA)             25.291417
EE.UU. (Russell 100)      24.619089
Reino Unido (FTSE 100)    23.422122
EE.UU. (NASDAQ)           18.626130
Alemania (DAX)            17.265559
EE.UU. (S&P 500)          14.200866
Japón (Nikkei 225)         9.664525
Name: final cumulative returns Benchmarks (%), dtype: float64
Inverse Momentum Ranking (Annual yield) of Benchmark Indices:
Japón (Nikkei 225)         9.664525
EE.UU. (S&P 500)          14.200866
Alemania (DAX)            17.265559
EE.UU. (NASDAQ)           18.626130
Reino Unido (FTSE 100)    23.422122
EE.UU. (Russell 100)      24.619089
EE.UU. (DJIA)             25.291417
México (IPC)              40.065119
Name: final cumulative returns Benchmarks (%), dtype: float64
No description has been provided for this image
No description has been provided for this image

Correlation¶

Correlation equation¶

Pearson correlation coefficient:

$$\rho_{X,Y} = \frac {cov(X,Y)}{\sigma_X \sigma_Y} = \frac{E[(X-\mu_X)(Y-\mu_Y)]}{\sigma_X \sigma_Y}$$

$$ \sigma_p^2 = w_A^2 \sigma_A^2 \;+\; w_B^2 \sigma_B^2 \;+\; 2\,w_A w_B\,\rho_{AB}\,\sigma_A \sigma_B $$

Stocks¶

In [83]:
functions.plot_interactive_heatmap_correlation(daily_returns, triangle='half')
functions.plot_extreme_correlations(daily_returns, num_pairs=10)
================================================================================
REGRESSION & VOLATILITY SUMMARY TABLE
================================================================================
  Correlation (r) R-Squared p-value Slope (Beta) Std Error Intercept (Alpha) Vol_GOOGL Vol_GOOG Significant? Vol_VOOG Vol_SPYG Vol_VUG Vol_QQQM Vol_VYM Vol_VTV Vol_VGT Vol_CME Vol_ASML Vol_NVDA Vol_KO Vol_TSM Vol_SOXX
Pair                                            
GOOGL vs GOOG 0.9979 0.9958 0.0000 0.9850 0.0030 0.0000 0.0191 0.0189 Yes nan nan nan nan nan nan nan nan nan nan nan nan nan
VOOG vs SPYG 0.9974 0.9948 0.0000 1.0013 0.0034 0.0000 nan nan Yes 0.0132 0.0132 nan nan nan nan nan nan nan nan nan nan nan
VUG vs VOOG 0.9910 0.9821 0.0000 0.9841 0.0062 0.0001 nan nan Yes 0.0132 nan 0.0132 nan nan nan nan nan nan nan nan nan nan
VUG vs SPYG 0.9884 0.9770 0.0000 0.9854 0.0071 0.0001 nan nan Yes nan 0.0132 0.0132 nan nan nan nan nan nan nan nan nan nan
SPYG vs QQQM 0.9866 0.9733 0.0000 0.9926 0.0077 -0.0001 nan nan Yes nan 0.0132 nan 0.0133 nan nan nan nan nan nan nan nan nan
VUG vs QQQM 0.9863 0.9729 0.0000 0.9894 0.0077 -0.0000 nan nan Yes nan nan 0.0132 0.0133 nan nan nan nan nan nan nan nan nan
VOOG vs QQQM 0.9862 0.9725 0.0000 0.9962 0.0078 -0.0001 nan nan Yes 0.0132 nan nan 0.0133 nan nan nan nan nan nan nan nan nan
VYM vs VTV 0.9839 0.9680 0.0000 0.9611 0.0082 -0.0000 nan nan Yes nan nan nan nan 0.0086 0.0084 nan nan nan nan nan nan nan
VGT vs QQQM 0.9759 0.9524 0.0000 0.8100 0.0085 0.0001 nan nan Yes nan nan nan 0.0133 nan nan 0.0160 nan nan nan nan nan nan
VGT vs SPYG 0.9720 0.9449 0.0000 0.8019 0.0091 0.0002 nan nan Yes nan 0.0132 nan nan nan nan 0.0160 nan nan nan nan nan nan
CME vs ASML -0.2532 0.0641 0.0000 -0.6253 0.1120 0.0013 nan nan Yes nan nan nan nan nan nan nan 0.0110 0.0272 nan nan nan nan
NVDA vs KO -0.2519 0.0634 0.0000 -0.0781 0.0141 0.0006 nan nan Yes nan nan nan nan nan nan nan nan nan 0.0318 0.0099 nan nan
TSM vs KO -0.2442 0.0596 0.0000 -0.0939 0.0175 0.0007 nan nan Yes nan nan nan nan nan nan nan nan nan nan 0.0099 0.0257 nan
SOXX vs CME -0.2406 0.0579 0.0000 -0.1117 0.0211 0.0009 nan nan Yes nan nan nan nan nan nan nan 0.0110 nan nan nan nan 0.0238
TSM vs CME -0.2195 0.0482 0.0000 -0.0944 0.0197 0.0010 nan nan Yes nan nan nan nan nan nan nan 0.0110 nan nan nan 0.0257 nan
VGT vs CME -0.1855 0.0344 0.0001 -0.1279 0.0318 0.0009 nan nan Yes nan nan nan nan nan nan 0.0160 0.0110 nan nan nan nan nan
NVDA vs CME -0.1810 0.0328 0.0001 -0.0628 0.0160 0.0009 nan nan Yes nan nan nan nan nan nan nan 0.0110 nan 0.0318 nan nan nan
SOXX vs KO -0.1750 0.0306 0.0002 -0.0726 0.0192 0.0005 nan nan Yes nan nan nan nan nan nan nan nan nan nan 0.0099 nan 0.0238
GOOGL vs CME -0.1610 0.0259 0.0005 -0.0930 0.0267 0.0009 0.0191 nan Yes nan nan nan nan nan nan nan 0.0110 nan nan nan nan nan
GOOG vs CME -0.1578 0.0249 0.0007 -0.0924 0.0271 0.0009 nan 0.0189 Yes nan nan nan nan nan nan nan 0.0110 nan nan nan nan nan
--------------------------------------------------
INTERPRETATION GUIDE:
--------------------------------------------------
• p-value Significance: Probability the correlation happened by chance. 
  Small values (e.g., < 1.42e-05) mean the relationship is mathematically solid.
• Significance?: 'Yes' if p_value < 0.05.
• Beta (Slope): The magnitude. A slope of 1.2 means for every 1% change in Stock A, 
  Stock B tends to move 1.2%.
• Alpha (Intercept): The 'excess' return of Stock B if Stock A's return was zero.
• Low Standard Error: Relationship is tight; Beta is a reliable predictor.
• High Standard Error: Lots of 'noise'; Beta is less reliable for hedging/pairs trading.
• Volatility (Vol):
  - High Vol + High Corr: High-octane comovers.
  - Low Vol + High Corr: Stable 'pairs trading' candidates.
  - Interpretation: If Vol Stock B > Vol Stock A, Beta will naturally be higher. 
    Check this to see if Beta is driven by correlation or just swing scale.

Indices & Portfolio¶

In [84]:
functions.plot_interactive_heatmap_correlation(merge_daily_returns, triangle='half')
functions.plot_extreme_correlations(merge_daily_returns, num_pairs=10)
================================================================================
REGRESSION & VOLATILITY SUMMARY TABLE
================================================================================
  Correlation (r) R-Squared p-value Slope (Beta) Std Error Intercept (Alpha) Vol_Reino Unido (FTSE 100) Vol_EE.UU. (Russell 100) Significant? Vol_México (IPC) Vol_Initial Portfolio Vol_EE.UU. (S&P 500) Vol_EE.UU. (DJIA) Vol_EE.UU. (NASDAQ)
Pair                            
Reino Unido (FTSE 100) vs EE.UU. (Russell 100) 0.9990 0.9981 0.0000 0.9888 0.0023 0.0000 0.0109 0.0108 Yes nan nan nan nan nan
México (IPC) vs EE.UU. (Russell 100) 0.9685 0.9381 0.0000 0.7283 0.0100 -0.0001 nan 0.0108 Yes 0.0144 nan nan nan nan
Reino Unido (FTSE 100) vs México (IPC) 0.9656 0.9323 0.0000 1.2709 0.0182 0.0002 0.0109 nan Yes 0.0144 nan nan nan nan
Initial Portfolio vs México (IPC) 0.9473 0.8974 0.0000 0.7949 0.0143 -0.0005 nan nan Yes 0.0144 0.0172 nan nan nan
Reino Unido (FTSE 100) vs EE.UU. (S&P 500) 0.9064 0.8216 0.0000 0.7962 0.0198 -0.0001 0.0109 nan Yes nan nan 0.0096 nan nan
EE.UU. (Russell 100) vs EE.UU. (S&P 500) 0.9004 0.8107 0.0000 0.7991 0.0206 -0.0001 nan 0.0108 Yes nan nan 0.0096 nan nan
Initial Portfolio vs EE.UU. (Russell 100) 0.8918 0.7953 0.0000 0.5627 0.0152 -0.0004 nan 0.0108 Yes nan 0.0172 nan nan nan
Initial Portfolio vs Reino Unido (FTSE 100) 0.8840 0.7815 0.0000 0.5636 0.0159 -0.0004 0.0109 nan Yes nan 0.0172 nan nan nan
México (IPC) vs EE.UU. (S&P 500) 0.7916 0.6266 0.0000 0.5283 0.0217 -0.0001 nan nan Yes 0.0144 nan 0.0096 nan nan
EE.UU. (DJIA) vs EE.UU. (NASDAQ) 0.7148 0.5110 0.0000 0.5083 0.0265 0.0002 nan nan Yes nan nan nan 0.0103 0.0074
--------------------------------------------------
INTERPRETATION GUIDE:
--------------------------------------------------
• p-value Significance: Probability the correlation happened by chance. 
  Small values (e.g., < 1.42e-05) mean the relationship is mathematically solid.
• Significance?: 'Yes' if p_value < 0.05.
• Beta (Slope): The magnitude. A slope of 1.2 means for every 1% change in Stock A, 
  Stock B tends to move 1.2%.
• Alpha (Intercept): The 'excess' return of Stock B if Stock A's return was zero.
• Low Standard Error: Relationship is tight; Beta is a reliable predictor.
• High Standard Error: Lots of 'noise'; Beta is less reliable for hedging/pairs trading.
• Volatility (Vol):
  - High Vol + High Corr: High-octane comovers.
  - Low Vol + High Corr: Stable 'pairs trading' candidates.
  - Interpretation: If Vol Stock B > Vol Stock A, Beta will naturally be higher. 
    Check this to see if Beta is driven by correlation or just swing scale.

Covariance¶

  • La matriz de covarianza es la base para calcular la varianza del portafolio:

$$ \sigma_p^2 = w^T \Sigma w $$

donde:

  • w = vector de pesos del portafolio.
  • $\Sigma = matriz$ de covarianzas.
  • Un gestor de portafolios busca combinaciones de activos con baja covarianza para reducir la volatilidad total manteniendo el rendimiento esperado.
  • Covariance:$$ \text{Cov}(X,Y) = E[(X - \mu_X)(Y - \mu_Y)]$$
  • Values:
    • Positive → one rises, other rises too.
    • Negative → one rises, other goes down.
    • near 0 → there's no clear relationtip.

Stocks¶

In [85]:
# Plot ANNUALIZED variance/covariance Matrix =  returns.cov() * 252
functions.plot_annualized_covariance_heatmap(daily_returns, triangle='half')

Indices & Portfolio¶

In [86]:
# Plot ANNUALIZED variance/covariance Matrix =  returns.cov() * 252
functions.plot_annualized_covariance_heatmap(merge_daily_returns, triangle='half')

Efficient Frontier (Sharpe Ratio)¶

$$ Sharpe = \frac{R_p - R_f}{\sigma_p} = \frac{\mu_p - r_f}{\sigma_p}$$

Where:

  • $R_p$: Expected Portfolio Return, $\mu$
  • $R_f$: Risk Free Rate (can be 0 if ignored)
  • $\sigma_p$: Portfolio Risk (StdDev)

Long-only: weights $\,w_i\in[0,1]$ y $\sum w_i=1$.

  • Only purchases, no leverage (by shorting)
  • Pros: Less operating risk
  • Cons: Frontier less efficient than with shorts (due leverage)

Shorts allowed: weights $\,w_i\in[-1,1]$ y $\sum w_i=1 \text{ (or similar)}$.

  • One can short sell and compensate with long positions
  • Pros: Theoretical improvement of the Frontier with more options
  • Cons:: Implied leverage, Margin requirements, Costs and Operational Risk

Stocks¶

In [149]:
# --- EFFICIENT FRONTIER & CAPITAL MARKET LINE ANALYSIS---

import functions # => .../functions.ipynb     file attached
importlib.reload(functions) # Reloads the module

functions.run_full_frontier_analysis(rets=daily_returns, 
                          curr_port_weights = weights_df['Weights'], 
                          curr_port_vol = portfolio_annualized_volatility, 
                          curr_port_ret = portfolio_annualized_return,
                          mean_ann = daily_returns.mean() * 252, 
                          cov_ann = daily_returns.cov() * 252, 
                          rf_default = risk_free,
                          long_only = True,  # Set to True for standard long-only constraints
                          portfolio_value = Total_invested, #100000
                          yields = yields/100
                          )
VBox(children=(HBox(children=(FloatText(value=4.152, description='RF Rate %:'), FloatText(value=5.0, descripti…

Number of starts: To avoid local minimas. 10-25 normal (fast and robust), 50-100 (for large amount of stocks)

In [148]:
no_starts = 25
no_simul = 2000000
In [90]:
weights_optimal, vol_ret_sr_optimal = functions.efficient_froentier_sharp_ratio(
                daily_returns, 
                [portfolio_annualized_volatility, portfolio_annualized_return], 
                daily_returns.mean()*252, # Simple Arithmetic Mean Annualization
                daily_returns.cov()*252, #Annualized Covariance
                risk_free, #Risk Free
                True, # Long only = True, Short allowed = False
                no_starts, # no. of starts (25 default)
                no_simul, # no. of simulations
                123 # seed
                )
Optimizing Sharpe…

Optimum Weights (%) - Tangency Portfolio
Ticker
AAPL      0.00
ADBE      0.00
ASML      0.00
BAC       1.45
BRK-B     0.00
CAT       2.55
CME      19.14
CRWD      0.00
DIS       0.00
GLD      39.85
GOOG      0.00
GOOGL    11.69
HD        0.00
KO       12.11
LMT       0.10
MAR       0.00
MCD       0.00
META      0.00
MSFT      0.00
NFLX      0.00
NU        0.00
NVDA      0.00
OMAB      1.50
PBR       0.00
PLTR      8.47
PSA       0.00
QQQM      0.00
QSR       0.00
QYLD      0.00
SERV      0.78
SOFI      1.23
SOXX      0.00
SPG       0.00
SPYD      0.00
SPYG      0.00
TSLA      0.00
TSM       1.13
UNH       0.00
VGT       0.00
VOO       0.00
VOOG      0.00
VOOV      0.00
VTV       0.00
VUG       0.00
VYM       0.00

Optimum Sharpe Ratio: 3.040
Expected Annual Return: 42.80%
Annual Risk: 12.71%

Simulating random Portfolios…

Simulated Portfolio with Minimum Volatility:
Volatility: 11.44%
Return: 21.65%
Sharpe Ratio: 1.529

Simulated Portfolio with Maximum Return:
Volatility: 69.42%
Return: 70.52%
Sharpe Ratio: 0.956

Current Portfolio:
Volatility: 26.13%
Return: 38.18%
Sharpe Ratio: 1.302
No description has been provided for this image

Positions from current to optimal¶

In [91]:
# Recall Total Invested
Investment = Total_invested

# Current Portolio Weights and Positions
weights_df

# Optimal Weights from Efficient Frontier
weights_optimal

# Merge DataFrames
weights_df_Optimal = pd.merge(weights_df, weights_optimal, left_index=True, right_index=True, how='outer')

# New Investment (USD)
weights_df_Optimal['New Investment usd'] = weights_df_Optimal['Optimal Weights'] * Investment

# New QTY
weights_df_Optimal['New QTY'] = weights_df_Optimal['New Investment usd'] / weights_df_Optimal['Close Price']

# Difference in QTY or buy/sell position
weights_df_Optimal['Position_to_Optimal'] = weights_df_Optimal['New QTY'] - weights_df_Optimal['Current QTY']

weights_df_Optimal = round(weights_df_Optimal, 3)
display(weights_df_Optimal)

print(f"Current Investment: $ {round(Investment, 2)}")
print(f"New Investment: $ {round(weights_df_Optimal['New Investment usd'].sum(), 2)}")
Current QTY Close Price Investment Weights Optimal Weights New Investment usd New QTY Position_to_Optimal
Ticker
AAPL 17.040 267.26 4554.121 0.066 0.000 0.000 0.000 -17.040
ADBE 0.465 331.56 154.205 0.002 0.000 0.000 0.000 -0.465
ASML 0.264 1228.19 324.451 0.005 0.000 0.000 0.000 -0.264
BAC 1.469 56.89 83.563 0.001 0.014 998.060 17.544 16.075
BRK-B 0.245 498.52 122.028 0.002 0.000 0.000 0.000 -0.245
CAT 0.334 616.10 205.932 0.003 0.026 1755.209 2.849 2.515
CME 2.000 275.06 550.120 0.008 0.191 13174.395 47.896 45.896
CRWD 1.455 456.55 664.399 0.010 0.000 0.000 0.000 -1.455
DIS 13.029 114.07 1486.232 0.022 0.000 0.000 0.000 -13.029
GLD 6.449 408.76 2636.236 0.038 0.398 27429.449 67.104 60.655
GOOG 1.614 317.32 512.100 0.007 0.000 0.000 0.000 -1.614
GOOGL 2.012 316.54 636.790 0.009 0.117 8046.430 25.420 23.408
HD 0.021 344.09 7.210 0.000 0.000 0.000 0.000 -0.021
KO 6.227 67.94 423.063 0.006 0.121 8335.524 122.689 116.462
LMT 0.319 511.57 163.252 0.002 0.001 68.832 0.135 -0.185
MAR 0.457 311.03 142.130 0.002 0.000 0.000 0.000 -0.457
MCD 1.054 299.86 315.954 0.005 0.000 0.000 0.000 -1.054
META 1.578 658.79 1039.452 0.015 0.000 0.000 0.000 -1.578
MSFT 19.752 472.85 9339.751 0.136 0.000 0.000 0.000 -19.752
NFLX 1.197 91.46 109.498 0.002 0.000 0.000 0.000 -1.197
NU 13.623 17.94 244.388 0.004 0.000 0.000 0.000 -13.623
NVDA 100.529 188.12 18911.482 0.275 0.000 0.000 0.000 -100.529
OMAB 2.164 109.26 236.470 0.003 0.015 1032.476 9.450 7.285
PBR 87.444 11.74 1026.597 0.015 0.000 0.000 0.000 -87.444
PLTR 21.024 174.04 3658.947 0.053 0.085 5830.048 33.498 12.475
PSA 0.190 260.90 49.624 0.001 0.000 0.000 0.000 -0.190
QQQM 6.376 254.43 1622.126 0.024 0.000 0.000 0.000 -6.376
QSR 5.255 66.74 350.734 0.005 0.000 0.000 0.000 -5.255
QYLD 100.860 17.76 1791.281 0.026 0.000 0.000 0.000 -100.860
SERV 3.365 12.68 42.666 0.001 0.008 536.888 42.341 38.976
SOFI 5.320 29.28 155.765 0.002 0.012 846.630 28.915 23.595
SOXX 3.046 318.06 968.755 0.014 0.000 0.000 0.000 -3.046
SPG 0.199 183.11 36.404 0.001 0.000 0.000 0.000 -0.199
SPYD 3.646 43.69 159.305 0.002 0.000 0.000 0.000 -3.646
SPYG 1.019 107.16 109.187 0.002 0.000 0.000 0.000 -1.019
TSLA 4.925 451.67 2224.304 0.032 0.000 0.000 0.000 -4.925
TSM 10.035 322.25 3233.660 0.047 0.011 777.799 2.414 -7.621
UNH 5.037 342.02 1722.644 0.025 0.000 0.000 0.000 -5.037
VGT 2.019 757.42 1529.254 0.022 0.000 0.000 0.000 -2.019
VOO 2.288 632.46 1447.284 0.021 0.000 0.000 0.000 -2.288
VOOG 6.296 446.51 2811.387 0.041 0.000 0.000 0.000 -6.296
VOOV 10.365 207.51 2150.748 0.031 0.000 0.000 0.000 -10.365
VTV 0.405 194.65 78.817 0.001 0.000 0.000 0.000 -0.405
VUG 1.008 488.45 492.502 0.007 0.000 0.000 0.000 -1.008
VYM 2.105 145.82 306.923 0.004 0.000 0.000 0.000 -2.105
Current Investment: $ 68831.74
New Investment: $ 68831.74

Including Risk Free Asset¶

Once you have the tangency portfolio 𝑤𝑡 (long-only), the fraction of your wealth 𝑋 to put into the risky portfolio is 𝑦.

Choose 𝑦 based on your target volatility, target return, or risk-aversion:

  • Objective 1. Target Volatility:

$$ 𝑦 = \frac{𝜎_𝑝}{𝜎_𝑡} $$

  • Objective 2. Target Return:

$$ 𝑦 = \frac{𝐸_𝑝 − 𝑅_𝑓}{𝐸[𝑅_𝑡] − 𝑅_𝑓} $$

  • Objective 3. Mean-Variance Utility (Expected Utility Funcion):

$$ U = E[R_p] - \frac{1}{2} \gamma \sigma_p^{2} $$

$$ 𝑦* = \frac{𝐸[𝑅_𝑡] − 𝑅_𝑓}{𝛾 𝜎_𝑡^{2}}$$

U : “Utility” — a scalar number representing the investor’s satisfaction from a portfolio. Its a parabolic function, higher gama the steeper the curve, thus more return expected for unit of risk.

𝛾 > 0: Risk aversion coefficient, a measure of how strongly the investor dislikes risk. Penalizes risk. Higher->more conservative.

image.png

Finally: invest 𝑦𝑋 in 𝑤_𝑡 and (1−𝑦)𝑋 in the risk-free asset, enforcing 0≤𝑦≤1 for long-only/no-borrowing.

In [92]:
# Objective 1. You want a target portfolio volatility 𝜎𝑝 :

# 𝜎𝑝 (target volatility):
# 90% of the Tangency portfolio volatility (can be any value)
target_volatility = vol_ret_sr_optimal[0] * 0.90

# The fraction of total wealth X invested in the tangency (risky) portfolio
y = target_volatility / vol_ret_sr_optimal[0]

print(f"Target Volatility: {target_volatility:.2%}")
print(f"The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: {y:.2%}, that is ${y*Total_invested:,.2f}usd")
print(f"The fraction invested in the risk-free asset (1-y) is: {1-y:.2%}, that is ${(1-y)*Total_invested:,.2f}usd")
ER_p = risk_free + y*(vol_ret_sr_optimal[1] - risk_free)
print(f"Expected Return E[Rp] = {ER_p:.2%}")
print(f"Sharpe Ratio = {(ER_p - risk_free) / target_volatility:.2f} ")
Target Volatility: 11.44%
The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: 90.00%, that is $61,948.57usd
The fraction invested in the risk-free asset (1-y) is: 10.00%, that is $6,883.17usd
Expected Return E[Rp] = 38.94%
Sharpe Ratio = 3.04 
In [93]:
# Objective 2. You want a target expected return Ep

# Ep (target Expected Return):
# 90% of the Tangency portfolio return (can be any value)
target_return = vol_ret_sr_optimal[1] * 0.90

# The fraction of total wealth X invested in the tangency (risky) portfolio
y = (target_return - risk_free) / (vol_ret_sr_optimal[1] - risk_free)

print(f"Target Expected Return: {target_return:.2%}")
print(f"The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: {y:.2%}, that is ${y*Total_invested:,.2f}usd")
print(f"The fraction invested in the risk-free asset (1-y) is: {1-y:.2%}, that is ${(1-y)*Total_invested:,.2f}usd")
sigma_p = y * vol_ret_sr_optimal[0]
print(f"Volatility: {sigma_p:.2%}")
print(f"Sharpe Ratio = {(target_return - risk_free)/sigma_p:.2f}")
Target Expected Return: 38.52%
The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: 88.93%, that is $61,209.13usd
The fraction invested in the risk-free asset (1-y) is: 11.07%, that is $7,622.61usd
Volatility: 11.31%
Sharpe Ratio = 3.04
In [94]:
# Objective 3. You maximize mean–variance Utility (risk aversion 𝛾)

# Gamma (risk aversion coefficient) :
# If 𝛾 is large, the investor is very risk averse — even small increases in variance are penalized heavily. 
# → They prefer portfolios with lower volatility, even if returns are modest.
# If 𝛾 is small, the investor is risk tolerant (or aggressive) — they are willing to accept more variance for more expected return.
gamma = 4

# a) Allowing borrowing (short)
# The fraction of total wealth X invested in the tangency (risky) portfolio
y_star = (vol_ret_sr_optimal[1] - risk_free) / (gamma * (vol_ret_sr_optimal[0]**2))
print("a) Allowing borrowing (short in risk asset):")
print(f"Mean-variance risk aversion coefficient: {gamma}")
print(f"The fraction of total wealth X invested in the tangency (risky) portfolio y* is: {y_star:.2%}, that is ${y_star * Total_invested:,.2f}usd")
print(f"The fraction invested in the risk-free asset (1-y*) is: {1-y:.2%}, that is ${(1-y_star) * Total_invested:,.2f}usd")
# E[R_p] = Rf + y* (E[Rt] - Rf)
ER_p = risk_free + (y_star * (vol_ret_sr_optimal[1] - risk_free))
sigma_p = y_star * vol_ret_sr_optimal[0]
print(f"Expected Return E[Rp] = {ER_p:.2%}")
print(f"Volatility sigma_p = {sigma_p:.2%}")
print(f"Sharpe Ratio = {(ER_p - risk_free)/sigma_p:.2}")

# b) For long-only, no-borrowing constraint: set 𝑦=min⁡(1,max⁡(0,𝑦∗)).
y = min(1, max(0, y_star))
print("\nb) For long-only, no-borrowing constraint:")
print(f"Mean-variance risk aversion coefficient: {gamma}")
print(f"The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: {y:.2%}, that is ${y*Total_invested:,.2f}usd")
print(f"The fraction invested in the risk-free asset (1-y) is: {1-y:.2%}, that is ${(1-y)*Total_invested:,.2f}usd")
ER_p = risk_free + (y * (vol_ret_sr_optimal[1] - risk_free))
sigma_p = y * vol_ret_sr_optimal[0]
print(f"Expected Return E[Rp] = {ER_p:.2%}")
print(f"Volatility sigma_p = {sigma_p:.2%}")
print(f"Sharpe Ratio = {(ER_p - risk_free)/sigma_p:.2}")
a) Allowing borrowing (short in risk asset):
Mean-variance risk aversion coefficient: 4
The fraction of total wealth X invested in the tangency (risky) portfolio y* is: 597.81%, that is $411,484.33usd
The fraction invested in the risk-free asset (1-y*) is: 11.07%, that is $-342,652.59usd
Expected Return E[Rp] = 235.20%
Volatility sigma_p = 76.00%
Sharpe Ratio = 3.0

b) For long-only, no-borrowing constraint:
Mean-variance risk aversion coefficient: 4
The fraction of total wealth X invested in the tangency (risky) portfolio 'y' is: 100.00%, that is $68,831.74usd
The fraction invested in the risk-free asset (1-y) is: 0.00%, that is $0.00usd
Expected Return E[Rp] = 42.80%
Volatility sigma_p = 12.71%
Sharpe Ratio = 3.0

The tangency portfolio has an enormously high risk-adjusted return — i.e., its Sharpe ratio(𝐸[𝑅𝑡]−𝑅𝑓)/𝜎𝑡=0.405/0.135=3.0(E[Rt]−Rf​)/σt=0.405/0.135=3.0 is extremely high.

gamma = 4 That’s a moderate risk aversion level. Even with moderate aversion, such a high Sharpe ratio pushes you toward aggressive leverage.

The tangency portfolio has an extremely high Sharpe (3.0). With moderate risk aversion 𝛾=4, the utility-maximizing solution is to lever the tangency portfolio heavily (556% of wealth) because the reward-to-risk tradeoff is so favorable.

image.png

Indices & Portfolio¶

In [154]:
# ANNUAL DIVIDEND YIELDS
yields_merge = functions.plot_annual_dividnd_yields(merge_daily_returns)
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
HTTP Error 404: 
Annual Dividend Yields from yf 'dividendYield' (%):
EE.UU. (S&P 500)          0
EE.UU. (NASDAQ)           0
EE.UU. (DJIA)             0
EE.UU. (Russell 100)      0
México (IPC)              0
Japón (Nikkei 225)        0
Alemania (DAX)            0
Reino Unido (FTSE 100)    0
Initial Portfolio         0
Name: dividendYield, dtype: int64
In [155]:
# --- EFFICIENT FRONTIER & CAPITAL MARKET LINE ANALYSIS---

import functions # => .../functions.ipynb     file attached
importlib.reload(functions) # Reloads the module

functions.run_full_frontier_analysis(rets = merge_daily_returns, 
                          curr_port_weights = weights_df['Weights'], 
                          curr_port_vol = portfolio_annualized_volatility, 
                          curr_port_ret = portfolio_annualized_return,
                          mean_ann = merge_daily_returns.mean() * 252, 
                          cov_ann = merge_daily_returns.cov() * 252, 
                          rf_default = risk_free,
                          long_only = True,  # Set to True for standard long-only constraints
                          portfolio_value = Total_invested, #100000
                          yields = yields_merge/100
                          )
VBox(children=(HBox(children=(FloatText(value=4.152, description='RF Rate %:'), FloatText(value=5.0, descripti…
In [95]:
weights_optimal, vol_ret_sr_optimal = functions.efficient_froentier_sharp_ratio(
                merge_daily_returns, 
                [portfolio_annualized_volatility, portfolio_annualized_return], 
                merge_daily_returns.mean()*252, # Simple Arithmetic Mean Annualization
                merge_daily_returns.cov()*252, #Annualized Covariance
                risk_free, #Risk Free
                True, # Long only = True, Short allowed = False
                no_starts, # no. of starts (25 default)
                no_simul, # no. of simulations
                123 # seed
                )
Optimizing Sharpe…

Optimum Weights (%) - Tangency Portfolio
EE.UU. (S&P 500)           0.00
EE.UU. (NASDAQ)           37.73
EE.UU. (DJIA)              7.24
EE.UU. (Russell 100)       0.00
México (IPC)               0.00
Japón (Nikkei 225)         0.00
Alemania (DAX)             1.48
Reino Unido (FTSE 100)     0.00
Initial Portfolio         53.55

Optimum Sharpe Ratio: 1.715
Expected Annual Return: 32.19%
Annual Risk: 16.35%

Simulating random Portfolios…

Simulated Portfolio with Minimum Volatility:
Volatility: 10.05%
Return: 12.42%
Sharpe Ratio: 0.823

Simulated Portfolio with Maximum Return:
Volatility: 24.61%
Return: 43.19%
Sharpe Ratio: 1.586

Current Portfolio:
Volatility: 26.13%
Return: 38.18%
Sharpe Ratio: 1.302
No description has been provided for this image

**TO-DO: Include GUI and "make your own portfolio" with a variety of Stocks to choose.¶

(see Frontera Eficiente for GUI)

Indicators: alpha, Beta, R^2, SR, Sortino, VaR, CVaR - vs with Benchmark¶

Métrica Valor Óptimo / Bueno Interpretación
Alpha (α) > 0 (ideal: +2% a +10% anual) Exceso de retorno sobre lo esperado por el CAPM; positivo indica valor agregado.
Beta (β) ≈ 1 (mercado), <1 defensivo, >1 agresivo Sensibilidad al mercado; >1 = más volátil, <1 = más estable.
R² (correlación) > 0.8 (80% o más) El portafolio se mueve casi igual que el benchmark (muy “indexado”).
R² (correlación) 0.6 – 0.8 Buena relación con el mercado, pero con diferencias notables.
R² (correlación) < 0.30 El portafolio se comporta muy distinto al mercado (alta independencia).
µ (Retorno anual) > 8% estable, > 15% agresivo Rendimiento esperado anualizado del portafolio.
σ (Volatilidad anual) 10%–20% moderado, <10% defensivo, >25% muy riesgoso Mide el riesgo total (desviación estándar).
Sharpe Ratio > 1 bueno, > 1.5 muy bueno, > 2 excelente Retorno ajustado por riesgo total.
Sortino Ratio > 2 excelente Retorno ajustado por riesgo a la baja (mejor si >> Sharpe).
VaR (95%, 1d) < 2% Pérdida máxima esperada en un día con 95% de confianza.
CVaR (95%, 1d) < 3% Pérdida promedio en los peores días (cola izquierda de la distribución).
In [ ]:
#recall
display(benchmark_indices)
{'EE.UU. (S&P 500)': '^GSPC',
 'EE.UU. (NASDAQ)': '^IXIC',
 'EE.UU. (DJIA)': '^DJI',
 'EE.UU. (Russell 100)': '^RUI',
 'México (IPC)': '^MXX',
 'Japón (Nikkei 225)': '^N225',
 'Alemania (DAX)': '^GDAXI',
 'Reino Unido (FTSE 100)': '^FTSE'}
In [ ]:
# Choose a benchmark to compare for alpha, beta, R2...
index_benchmark = 0

functions.indicators(tickers, start_date, today, benchmark_indices, index_benchmark,
                     risk_free, portfolio_weights=pd.Series(weights_df['Weights']), no_starts=no_starts)

** TO-DO: EN Metricas Anualizadas incluir de forma automatica los otros 6 benchmarks¶

TO-DO: CAPM (en archivo de Indicadores)¶

CAPM (Capital Asset Pricing Model)¶

CAPM Equation:

$$ r_{i} = r_{f} + \beta_{im}*(r_{m}-r_{f}) $$

where :
$r_i = $ Expected Asset Return
$r_f = $ Risk-free asset Return
$\beta_{i,m} = $ Asset Beta w.r.t market
$r_m = $ Market Return


Risk-free is the minimum Return an Investor can accept.
Difference between $r_{m}$ and $r_{f}$ is the Premium that the investor recieves by taking the risk (equity risk Premium).
$\beta$ measures the cuantity of Risk of an asset with respect to the Market.

Asset Beta ( $\beta$ )¶

$$\beta = \frac{\text{Cov(}r_A, r_m\text{)}}{Var(r_m)} = \frac{\sigma_{A,m}}{\sigma_m^2} = \frac{ρ_{A,m} σ_m σ_A}{\sigma_m^2} = \frac{ρ_{A,m}σ_A}{\sigma_m}$$

Candles¶